From 09c62587f609d1e558a32aef180854c969179375 Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Sun, 5 Dec 2021 11:17:23 +0200 Subject: [PATCH 1/7] Add string schema properties Charset, pattern, blank --- src/malli/core.cljc | 71 ++++++++++++++++++++++++++++++++++++++- src/malli/impl/util.cljc | 7 ++++ test/malli/core_test.cljc | 42 +++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/malli/core.cljc b/src/malli/core.cljc index a8ce6c761..2070f4be4 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -570,6 +570,75 @@ (when-let [ns-name (some-> properties :namespace name)] (fn [x] (= (namespace x) ns-name)))) +;; +;; string schema helpers +;; + +#?(:clj + (defn -charset-predicate + [o] + (case o + :digit #(Character/isDigit ^char %) + :letter #(Character/isLetter ^char %) + :letter-or-digit #(Character/isLetterOrDigit ^char %) + :alphanumeric #(Character/isLetterOrDigit ^char %) + :alphabetic #(Character/isAlphabetic (int %)) + (cond + (set? o) (miu/-some-pred (map -charset-predicate o)) + (char? o) #(= ^char o %) + (fn? o) o + (nil? o) nil + :else (throw (ex-info "Invalid string predicate" {:pred o})))))) + +#?(:clj + (defn string-char-predicate + [p] + (fn charset-pred ^Boolean [^String s] + (let [n (.length s)] + (loop [i 0] + (if (= i n) + true + (if (p (.charAt s (unchecked-int i))) + (recur (unchecked-inc i)) + false))))))) + +#?(:clj + (defn find-blank-method + [] + (try + (.getMethod String "isBlank" (into-array Class [])) + #(.isBlank ^String %) + (catch Exception _ + (require 'clojure.string) + clojure.string/blank?)))) + +#?(:clj + (def blank? (find-blank-method))) + +#?(:clj + (defn -string-predicates + ([{:keys [charset pattern non-blank]}] + (let [pattern + (when pattern + (let [pattern (re-pattern pattern)] + #(.find (.matcher ^Pattern pattern ^String %)))) + charset + (when charset + (let [p (-charset-predicate charset)] + (string-char-predicate p))) + non-blank (when non-blank #(not (blank? %)))] + (-> non-blank + (miu/-maybe-and charset) + (miu/-maybe-and pattern)))))) + +#?(:clj + (defn -string-property-pred + [] + (fn [properties] + (miu/-maybe-and + ((-min-max-pred #(.length ^String %)) properties) + (-string-predicates properties))))) + ;; ;; Schemas ;; @@ -625,7 +694,7 @@ (defn -nil-schema [] (-simple-schema {:type :nil, :pred nil?})) (defn -any-schema [] (-simple-schema {:type :any, :pred any?})) -(defn -string-schema [] (-simple-schema {:type :string, :pred string?, :property-pred (-min-max-pred count)})) +(defn -string-schema [] (-simple-schema {:type :string, :pred string?, :property-pred #?(:clj (-string-property-pred) :cljs (-min-max-pred count))})) (defn -int-schema [] (-simple-schema {:type :int, :pred int?, :property-pred (-min-max-pred nil)})) (defn -double-schema [] (-simple-schema {:type :double, :pred double?, :property-pred (-min-max-pred nil)})) (defn -boolean-schema [] (-simple-schema {:type :boolean, :pred boolean?})) diff --git a/src/malli/impl/util.cljc b/src/malli/impl/util.cljc index ff4902d8b..90cb1bb59 100644 --- a/src/malli/impl/util.cljc +++ b/src/malli/impl/util.cljc @@ -65,3 +65,10 @@ (def ^{:arglists '([[& preds]])} -some-pred #?(:clj (-pred-composer or 16) :cljs (fn [preds] (fn [x] (boolean (some #(% x) preds)))))) + +(defn -maybe-and + [f g] + (cond + (and f g) #(and (f %) (g %)) + f f + g g)) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 2267fae51..f5dc1020f 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -2622,3 +2622,45 @@ (is (= ["1"] (m/-vmap str (subvec [1 2] 0 1)))) (is (= ["1"] (m/-vmap str (lazy-seq [1])))) (is (= ["1" "2"] (m/-vmap str [1 2])))) + +#?(:clj + (deftest string + (testing "pattern" + (let [s (m/schema [:string {:pattern "foo"}])] + (is (true? (m/validate s "foo"))) + (is (true? (m/validate s "afoo"))) + (is (true? (m/validate s "fooa"))) + (is (false? (m/validate s "foao")))) + (let [s (m/schema [:string {:pattern "^foo"}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s "afoo"))) + (is (true? (m/validate s "fooa"))) + (is (false? (m/validate s "foao"))))) + (testing "charset" + (let [s (m/schema [:string {:charset :alphabetic}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s "fo1o")))) + (let [s (m/schema [:string {:charset :letter}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s "fo1o")))) + (let [s (m/schema [:string {:charset :letter-or-digit}])] + (is (true? (m/validate s "foo"))) + (is (true? (m/validate s "fo0"))) + (is (false? (m/validate s "f-1o")))) + (let [s (m/schema [:string {:charset #{\- :letter-or-digit}}])] + (is (true? (m/validate s "foo"))) + (is (true? (m/validate s "fo0"))) + (is (true? (m/validate s "f-1o"))) + (is (false? (m/validate s "f?1o"))))) + (testing "non blank" + (let [s (m/schema [:string {:non-blank true}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s ""))) + (is (false? (m/validate s " "))))) + (testing "Combined" + (let [s (m/schema [:string {:non-blank true :pattern "foo" :charset :letter-or-digit}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s ""))) + (is (false? (m/validate s " "))) + (is (false? (m/validate s " foo "))) + (is (true? (m/validate s "foo0"))))))) From dc8eedac99cf8384933dcdb30f7821b57d377e9a Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Mon, 6 Dec 2021 11:02:00 +0200 Subject: [PATCH 2/7] Add cljs implementation --- src/malli/core.cljc | 115 ++++++++++++++++++++------------------ test/malli/core_test.cljc | 81 +++++++++++++-------------- 2 files changed, 101 insertions(+), 95 deletions(-) diff --git a/src/malli/core.cljc b/src/malli/core.cljc index 2070f4be4..8045a0cbc 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -574,33 +574,38 @@ ;; string schema helpers ;; -#?(:clj - (defn -charset-predicate - [o] - (case o - :digit #(Character/isDigit ^char %) - :letter #(Character/isLetter ^char %) - :letter-or-digit #(Character/isLetterOrDigit ^char %) - :alphanumeric #(Character/isLetterOrDigit ^char %) - :alphabetic #(Character/isAlphabetic (int %)) - (cond - (set? o) (miu/-some-pred (map -charset-predicate o)) - (char? o) #(= ^char o %) - (fn? o) o - (nil? o) nil - :else (throw (ex-info "Invalid string predicate" {:pred o})))))) - -#?(:clj - (defn string-char-predicate - [p] - (fn charset-pred ^Boolean [^String s] - (let [n (.length s)] - (loop [i 0] - (if (= i n) - true - (if (p (.charAt s (unchecked-int i))) - (recur (unchecked-inc i)) - false))))))) +#?(:cljs (defn -numeric-char? [c] (and (< 47 c) (< c 58)))) +#?(:cljs (defn -upper-alpha-char? [c] (and (< 64 c) (< c 91)))) +#?(:cljs (defn -lower-alpha-char? [c] (and (< 96 c) (< c 123)))) +#?(:cljs (defn -letter? [c] (or (-lower-alpha-char? c) (-upper-alpha-char? c)))) +#?(:cljs (defn -alphanumeric? [c] (or (-letter? c) (-numeric-char? c)))) + +(defn -charset-predicate + [o] + (case o + :digit #?(:clj #(Character/isDigit ^char %) :cljs -numeric-char?) + :letter #?(:clj #(Character/isLetter ^char %) :cljs -letter?) + :letter-or-digit #?(:clj #(Character/isLetterOrDigit ^char %) :cljs -alphanumeric?) + :alphanumeric #?(:clj #(Character/isLetterOrDigit ^char %) :cljs -alphanumeric?) + :alphabetic #?(:clj #(Character/isAlphabetic (int %)) :cljs -letter?) + (cond + (set? o) (miu/-some-pred (mapv -charset-predicate o)) + (char? o) #?(:clj #(= ^char o %) :cljs (let [i (.charCodeAt o 0)] #(= i %))) + (fn? o) o + (nil? o) nil + :else (throw (ex-info "Invalid string predicate" {:pred o}))))) + +(defn string-char-predicate + [p] + (fn charset-pred ^Boolean [^String s] + (let [n #?(:clj (.length s) :cljs (.-length s))] + (loop [i 0] + (if (= i n) + true + (if (p #?(:clj (.charAt s (unchecked-int i)) + :cljs (.charCodeAt s (unchecked-int i)))) + (recur (unchecked-inc i)) + false)))))) #?(:clj (defn find-blank-method @@ -612,32 +617,34 @@ (require 'clojure.string) clojure.string/blank?)))) -#?(:clj - (def blank? (find-blank-method))) - -#?(:clj - (defn -string-predicates - ([{:keys [charset pattern non-blank]}] - (let [pattern - (when pattern - (let [pattern (re-pattern pattern)] - #(.find (.matcher ^Pattern pattern ^String %)))) - charset - (when charset - (let [p (-charset-predicate charset)] - (string-char-predicate p))) - non-blank (when non-blank #(not (blank? %)))] - (-> non-blank - (miu/-maybe-and charset) - (miu/-maybe-and pattern)))))) - -#?(:clj - (defn -string-property-pred - [] - (fn [properties] - (miu/-maybe-and - ((-min-max-pred #(.length ^String %)) properties) - (-string-predicates properties))))) +#?(:clj (def blank? (find-blank-method)) + :cljs (defn blank? [^String s] (zero? (.-length (.trim s))))) + +(defn -string-predicates + ([{:keys [charset pattern non-blank]}] + (let [pattern + (when pattern + (let [pattern (re-pattern pattern)] + #?(:clj #(.find (.matcher ^Pattern pattern ^String %)) + :cljs #(boolean (re-find pattern %))))) + charset + (when charset + (let [p (-charset-predicate charset)] + (string-char-predicate p))) + non-blank (when non-blank #(not (blank? %)))] + (-> non-blank + (miu/-maybe-and charset) + (miu/-maybe-and pattern))))) + +(defn -string-property-pred + [] + (fn [properties] + (miu/-maybe-and + ((-min-max-pred + #?(:clj #(.length ^String %) + :cljs #(.-length ^String %))) + properties) + (-string-predicates properties)))) ;; ;; Schemas @@ -694,7 +701,7 @@ (defn -nil-schema [] (-simple-schema {:type :nil, :pred nil?})) (defn -any-schema [] (-simple-schema {:type :any, :pred any?})) -(defn -string-schema [] (-simple-schema {:type :string, :pred string?, :property-pred #?(:clj (-string-property-pred) :cljs (-min-max-pred count))})) +(defn -string-schema [] (-simple-schema {:type :string, :pred string?, :property-pred (-string-property-pred)})) (defn -int-schema [] (-simple-schema {:type :int, :pred int?, :property-pred (-min-max-pred nil)})) (defn -double-schema [] (-simple-schema {:type :double, :pred double?, :property-pred (-min-max-pred nil)})) (defn -boolean-schema [] (-simple-schema {:type :boolean, :pred boolean?})) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index f5dc1020f..83c47f7c3 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -2623,44 +2623,43 @@ (is (= ["1"] (m/-vmap str (lazy-seq [1])))) (is (= ["1" "2"] (m/-vmap str [1 2])))) -#?(:clj - (deftest string - (testing "pattern" - (let [s (m/schema [:string {:pattern "foo"}])] - (is (true? (m/validate s "foo"))) - (is (true? (m/validate s "afoo"))) - (is (true? (m/validate s "fooa"))) - (is (false? (m/validate s "foao")))) - (let [s (m/schema [:string {:pattern "^foo"}])] - (is (true? (m/validate s "foo"))) - (is (false? (m/validate s "afoo"))) - (is (true? (m/validate s "fooa"))) - (is (false? (m/validate s "foao"))))) - (testing "charset" - (let [s (m/schema [:string {:charset :alphabetic}])] - (is (true? (m/validate s "foo"))) - (is (false? (m/validate s "fo1o")))) - (let [s (m/schema [:string {:charset :letter}])] - (is (true? (m/validate s "foo"))) - (is (false? (m/validate s "fo1o")))) - (let [s (m/schema [:string {:charset :letter-or-digit}])] - (is (true? (m/validate s "foo"))) - (is (true? (m/validate s "fo0"))) - (is (false? (m/validate s "f-1o")))) - (let [s (m/schema [:string {:charset #{\- :letter-or-digit}}])] - (is (true? (m/validate s "foo"))) - (is (true? (m/validate s "fo0"))) - (is (true? (m/validate s "f-1o"))) - (is (false? (m/validate s "f?1o"))))) - (testing "non blank" - (let [s (m/schema [:string {:non-blank true}])] - (is (true? (m/validate s "foo"))) - (is (false? (m/validate s ""))) - (is (false? (m/validate s " "))))) - (testing "Combined" - (let [s (m/schema [:string {:non-blank true :pattern "foo" :charset :letter-or-digit}])] - (is (true? (m/validate s "foo"))) - (is (false? (m/validate s ""))) - (is (false? (m/validate s " "))) - (is (false? (m/validate s " foo "))) - (is (true? (m/validate s "foo0"))))))) +(deftest string-test + (testing "pattern" + (let [s (m/schema [:string {:pattern "foo"}])] + (is (true? (m/validate s "foo"))) + (is (true? (m/validate s "afoo"))) + (is (true? (m/validate s "fooa"))) + (is (false? (m/validate s "foao")))) + (let [s (m/schema [:string {:pattern "^foo"}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s "afoo"))) + (is (true? (m/validate s "fooa"))) + (is (false? (m/validate s "foao"))))) + (testing "charset" + (let [s (m/schema [:string {:charset :alphabetic}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s "fo1o")))) + (let [s (m/schema [:string {:charset :letter}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s "fo1o")))) + (let [s (m/schema [:string {:charset :letter-or-digit}])] + (is (true? (m/validate s "foo"))) + (is (true? (m/validate s "fo0"))) + (is (false? (m/validate s "f-1o")))) + (let [s (m/schema [:string {:charset #{\- :letter-or-digit}}])] + (is (true? (m/validate s "foo"))) + (is (true? (m/validate s "fo0"))) + (is (true? (m/validate s "f-1x"))) + (is (false? (m/validate s "f?1o"))))) + (testing "non blank" + (let [s (m/schema [:string {:non-blank true}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s ""))) + (is (false? (m/validate s " "))))) + (testing "Combined" + (let [s (m/schema [:string {:non-blank true :pattern "foo" :charset :letter-or-digit}])] + (is (true? (m/validate s "foo"))) + (is (false? (m/validate s ""))) + (is (false? (m/validate s " "))) + (is (false? (m/validate s " foo "))) + (is (true? (m/validate s "foo0")))))) From 82a3c4ed56d6db3bdb4fd34485b399819369282c Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Mon, 6 Dec 2021 15:23:21 +0200 Subject: [PATCH 3/7] Use fail instead of throw --- src/malli/core.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/malli/core.cljc b/src/malli/core.cljc index 8045a0cbc..c89a11f3a 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -593,7 +593,7 @@ (char? o) #?(:clj #(= ^char o %) :cljs (let [i (.charCodeAt o 0)] #(= i %))) (fn? o) o (nil? o) nil - :else (throw (ex-info "Invalid string predicate" {:pred o}))))) + :else (-fail! ::invalid-char-predicate {:pred o})))) (defn string-char-predicate [p] From 402034f77b9316c9a3677c5e852b60f123990861 Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Mon, 6 Dec 2021 16:55:43 +0200 Subject: [PATCH 4/7] Simplify charset default case --- src/malli/core.cljc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/malli/core.cljc b/src/malli/core.cljc index c89a11f3a..4e7b31a5f 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -591,9 +591,7 @@ (cond (set? o) (miu/-some-pred (mapv -charset-predicate o)) (char? o) #?(:clj #(= ^char o %) :cljs (let [i (.charCodeAt o 0)] #(= i %))) - (fn? o) o - (nil? o) nil - :else (-fail! ::invalid-char-predicate {:pred o})))) + :else (eval o)))) (defn string-char-predicate [p] From 3df68a15eab04f6e0d6edf194b9ffafa2c72c894 Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Mon, 6 Dec 2021 16:58:17 +0200 Subject: [PATCH 5/7] Collapse identical case --- src/malli/core.cljc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/malli/core.cljc b/src/malli/core.cljc index 4e7b31a5f..e8ab01bd4 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -585,8 +585,7 @@ (case o :digit #?(:clj #(Character/isDigit ^char %) :cljs -numeric-char?) :letter #?(:clj #(Character/isLetter ^char %) :cljs -letter?) - :letter-or-digit #?(:clj #(Character/isLetterOrDigit ^char %) :cljs -alphanumeric?) - :alphanumeric #?(:clj #(Character/isLetterOrDigit ^char %) :cljs -alphanumeric?) + (:alphanumeric :letter-or-digit) #?(:clj #(Character/isLetterOrDigit ^char %) :cljs -alphanumeric?) :alphabetic #?(:clj #(Character/isAlphabetic (int %)) :cljs -letter?) (cond (set? o) (miu/-some-pred (mapv -charset-predicate o)) From 7973683d7fd55b608357a3fc51ddf51e72e37fe2 Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Mon, 6 Dec 2021 16:58:42 +0200 Subject: [PATCH 6/7] Add string charset, pattern, blank support --- src/malli/generator.cljc | 51 +++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/malli/generator.cljc b/src/malli/generator.cljc index 1f338f842..9854f7d96 100644 --- a/src/malli/generator.cljc +++ b/src/malli/generator.cljc @@ -36,14 +36,45 @@ (defn- -double-gen [options] (gen/double* (merge {:infinite? false, :NaN? false} options))) -(defn- -string-gen [schema options] - (let [{:keys [min max]} (-min-max schema options)] +(def ^:private char-numeric (gen/fmap char (gen/choose 48 57))) + +(defn- -char-gen + [k] + (case k + :digit char-numeric + :letter gen/char-alpha + (:alphanumeric :letter-or-digit) gen/char-alphanumeric (cond - (and min (= min max)) (gen/fmap str/join (gen/vector gen/char min)) - (and min max) (gen/fmap str/join (gen/vector gen/char min max)) - min (gen/fmap str/join (gen/vector gen/char min (* 2 min))) - max (gen/fmap str/join (gen/vector gen/char 0 max)) - :else gen/string-alphanumeric))) + (set? k) + (let [chars (into [] (filter char? k)) + chars (gen/fmap chars (gen/choose 0 (dec (count chars)))) + gens (into [] (comp (remove char?) (map -char-gen)) k)] + (gen/one-of (conj gens chars)))))) + +#?(:clj + (defn- -string-from-regex [re] + (if-let [string-from-regex @(dynaload/dynaload 'com.gfredericks.test.chuck.generators/string-from-regex {:default nil})] + (string-from-regex (re-pattern (str/replace (str re) #"^\^?(.*?)(\$?)$" "$1"))) + (m/-fail! :test-chuck-not-available)))) + +(defn- -string-gen [schema options] + (let [{:keys [min max]} (-min-max schema options) + {:keys [charset pattern non-blank] + :or {charset :alphanumeric}} (m/properties schema options) + min (cond + (and min non-blank) (clojure.core/max min non-blank) + non-blank 1 + min min)] + (if pattern + #?(:clj (-string-from-regex pattern) :cljs (m/-fail! ::unsupported-generator)) + (let [seed (-char-gen charset) + gen (cond + (and min (= min max)) (gen/vector seed min) + (and min max) (gen/vector seed min max) + min (gen/vector seed min (* 2 min)) + max (gen/vector seed 0 max) + :else (gen/vector seed))] + (gen/fmap str/join gen))))) (defn- -coll-gen [schema f options] (let [{:keys [min max]} (-min-max schema options) @@ -101,10 +132,8 @@ #?(:clj (defn -re-gen [schema options] ;; [com.gfredericks/test.chuck "0.2.10"+] - (if-let [string-from-regex @(dynaload/dynaload 'com.gfredericks.test.chuck.generators/string-from-regex {:default nil})] - (let [re (or (first (m/children schema options)) (m/form schema options))] - (string-from-regex (re-pattern (str/replace (str re) #"^\^?(.*?)(\$?)$" "$1")))) - (m/-fail! :test-chuck-not-available)))) + (let [re (or (first (m/children schema options)) (m/form schema options))] + (-string-from-regex re)))) (defn -ref-gen [schema options] (let [gen* (delay (generator (m/deref-all schema) options))] From b4c04a738750281031ad8b480663a8e38760ef66 Mon Sep 17 00:00:00 2001 From: Ben Sless Date: Mon, 6 Dec 2021 23:05:06 +0200 Subject: [PATCH 7/7] Add pattern and charset to string json schema --- src/malli/json_schema.cljc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/malli/json_schema.cljc b/src/malli/json_schema.cljc index 3cf3bf656..a5eba6665 100644 --- a/src/malli/json_schema.cljc +++ b/src/malli/json_schema.cljc @@ -136,7 +136,18 @@ (defmethod accept :nil [_ _ _ _] {:type "null"}) (defmethod accept :string [_ schema _ _] - (merge {:type "string"} (-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minLength, :max :maxLength})))) + (let [props (-> schema m/properties) + pattern (case (:charset props) + :digit "^[0-9]*$" + :letter "^[a-zA-Z]*$" + (:alphanumeric :letter-or-digit) "^[a-zA-Z0-9]*$" + nil) + props (cond-> props pattern (assoc :pattern pattern))] + (merge + {:type "string"} + (-> props + (select-keys [:min :max :pattern]) + (set/rename-keys {:min :minLength, :max :maxLength}))))) (defmethod accept :int [_ schema _ _] (merge {:type "integer"} (-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minimum, :max :maximum}))))