init research

This commit is contained in:
2026-02-08 11:20:43 -10:00
commit bdf064f54d
3041 changed files with 1592200 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
(ns bb-test-runner
(:require
[clojure.test :as t]
[malli.clj-kondo-test]
[malli.core-test]
[malli.destructure-test]
[malli.dot-test]
[malli.error-test]
[malli.experimental-test]
[malli.generator-test]
[malli.instrument-test]
[malli.json-schema-test]
[malli.parser-test]
[malli.plantuml-test]
[malli.provider-test]
[malli.registry-test]
[malli.swagger-test]
[malli.transform-test]
[malli.util-test]))
(defn run-tests [& _args]
(let [{:keys [fail error]}
(t/run-tests
'malli.core-test
'malli.clj-kondo-test
'malli.destructure-test
'malli.dot-test
'malli.error-test
'malli.experimental-test
'malli.instrument-test
'malli.json-schema-test
;; 'malli.generator-test ;; skipped for now due to test.chuck incompatibility
'malli.parser-test
'malli.plantuml-test
'malli.provider-test
'malli.registry-test
'malli.swagger-test
'malli.transform-test
'malli.util-test)]
(when (or (pos? fail)
(pos? error))
(System/exit 1))))
+36
View File
@@ -0,0 +1,36 @@
(ns demo)
(require '[malli.dev.pretty :as pretty])
(def Adult
[:map
[:age [:int {:min 18}]]
[:home [:map
[:city :string]
[:zip :int]]]])
(comment
(pretty/explain
Adult
{:name "Endy"
:age 17
:home {:zip 33100}}))
(comment
(pretty/explain
[:map
[:id :int]
[:tags [:set :keyword]]
[:address [:map
[:street :string]
[:city :string]
[:zip :int]
[:lonlat [:tuple :double :double]]]]]
{:id "123"
:EXTRA "KEY"
:tags #{:artesan "coffee" :garden}
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip 33100
:lonlat [61.4858322, 23.7832851]}}))
+29
View File
@@ -0,0 +1,29 @@
(ns malli.assert-test
(:refer-clojure :exclude [assert])
(:require
[clojure.test :refer [deftest is]]
[malli.core :refer [assert]]))
(set! *assert* true)
(deftest assert-throws-test
(is (thrown? #?(:clj Exception, :cljs js/Error)
(assert :int "42" )))
(is (thrown? #?(:clj Exception, :cljs js/Error)
(assert int? "42" )))
(is (thrown? #?(:clj Exception, :cljs js/Error)
(assert string? 42)))
(is (thrown? #?(:clj Exception, :cljs js/Error)
(assert int? nil)))
(is (thrown? #?(:clj Exception, :cljs js/Error)
(assert [:map [:a int?]] {:a "42"})))
(is (thrown? #?(:clj Exception, :cljs js/Error)
(assert ::invalid-schema 42))))
(deftest assert-checked-and-does-not-throw
(is (= 42 (assert :int 42 )))
(is (= 42 (assert int? 42 )))
(is (= "42" (assert string? "42")))
(is (= nil (assert any? nil)))
(is (= {:a 42} (assert [:map [:a int?]] {:a 42}))))
+166
View File
@@ -0,0 +1,166 @@
(ns malli.clj-kondo-test
(:require [clojure.test :refer [deftest is testing]]
[malli.clj-kondo :as clj-kondo]
[malli.core :as m]
#?@(:clj [[clojure.java.io :as io]
[clojure.edn :as edn]])
[malli.util :as mu]))
(def Schema
(m/schema
[:map {:registry {::id string?
::price double?}}
::id
[::price {:optional true}]
[:name string?]
[:description [:maybe string?]]
[:tags {:optional true} [:set qualified-keyword?]]
[::y {:optional true} boolean?]
[:select-keys [:maybe [:select-keys [:map [:x int?] [:y int?]] [:x]]]]
[:xyz :any]
[:xyz2 [:maybe :any]]
[:xyz3 [:maybe :int]]
[:tuple-of-ints [:maybe [:tuple :int :int]]]
[:nested [:merge
[:map [:id ::id]]
[:map [:price ::price]]]]
[:string-type-enum [:maybe [:enum "b" "c"]]]
[:keyword-type-enum [:enum :a :b]]
[:any-type-enum [:enum :a "b" "c"]]
[:z [:vector [:map-of int? int?]]]]
{:registry (merge (m/default-schemas) (mu/schemas))}))
(defn kikka
([x] (* x x))
([x y & z] (apply + (* x y) z)))
(m/=> kikka [:function
[:=> [:cat :int] [:int {:min 0}]]
[:=> [:cat :int :int [:* :int]] :int]])
(defn kikka2
([x] (* x x))
([x y & z] (apply + (* x y) z)))
(m/=> kikka2 [:function
[:-> :int [:int {:min 0}]]
[:-> :int :int [:* :int] :int]])
(defn siren [f coll]
(into {} (map (juxt f identity) coll)))
(m/=> siren [:=> [:cat ifn? coll?] map?])
(defn clj-kondo-issue-1922-1 [_x])
(m/=> clj-kondo-issue-1922-1
[:=> [:cat [:map [:keys [:+ :keyword]]]] :nil])
(defn clj-kondo-issue-1922-2 [_x])
(m/=> clj-kondo-issue-1922-2
[:=> [:cat [:map [:keys [:* :int]]]] :nil])
(defn clj-kondo-issue-1922-3 [_x])
(m/=> clj-kondo-issue-1922-3
[:=> [:cat [:map [:keys [:? :string]]]] :nil])
(defn clj-kondo-issue-1922-4 [_x])
(m/=> clj-kondo-issue-1922-4
[:function
[:=> [:cat :int :int] :nil]
[:=> [:cat :int :int [:repeat :int]] :nil]])
(defn clj-kondo-issue-836-1
"Predicate `:fn` schema's should be type-checked as expecting to be passed `:any`, not a fn."
[x y z] (* x y z))
(m/=> clj-kondo-issue-836-1 [:=> [:cat int? [:fn #(int? %)] int?] [:fn #(int? %)]])
(defn- cljk-collect-for-test
"Collect up all of the clj-kondo linters generated for fn's in this test ns."
[]
(-> 'malli.clj-kondo-test
#?(:clj (clj-kondo/collect))
#?(:cljs (clj-kondo/collect-cljs))
(clj-kondo/linter-config)
(get-in [:linters :type-mismatch :namespaces])))
(deftest clj-kondo-integration-test
(is (= {:op :keys,
:opt {::price :double, :tags :set, ::y :boolean},
:req {::id :string,
:name :string,
:description :nilable/string,
:select-keys {:op :keys, :req {:x :int} :nilable true},
:xyz :any
:xyz2 :any
:xyz3 :nilable/int
:nested {:op :keys, :req {:id :string, :price :double}},
:string-type-enum :nilable/string
:keyword-type-enum :keyword
:any-type-enum :any
:z :vector
:tuple-of-ints :nilable/seqable}}
(clj-kondo/transform Schema)))
(let [expected-out
{'malli.clj-kondo-test
{'kikka
{:arities {1 {:args [:int],
:ret :int},
:varargs {:args [:int :int {:op :rest :spec :int}],
:ret :int,
:min-arity 2}}}
'kikka2
{:arities {1 {:args [:int],
:ret :int},
:varargs {:args [:int :int {:op :rest :spec :int}],
:ret :int,
:min-arity 2}}}
'siren
{:arities {2 {:args [:ifn :coll], :ret :map}}}
'clj-kondo-issue-1922-1
{:arities {1 {:args [{:op :keys
:req {:keys :seqable}}]
:ret :nil}}}
'clj-kondo-issue-1922-2
{:arities {1 {:args [{:op :keys
:req {:keys :seqable}}]
:ret :nil}}}
'clj-kondo-issue-1922-3
{:arities {1 {:args [{:op :keys
:req {:keys :seqable}}]
:ret :nil}}}
'clj-kondo-issue-1922-4
{:arities {2 {:args [:int :int]
:ret :nil}
:varargs {:args [:int :int {:op :rest :spec :int}]
:ret :nil
:min-arity 2}}}
;; should output `:any` for `:fn` predicate schema's, not `:fn`
'clj-kondo-issue-836-1
{:arities {3 {:args [:int :any :int], :ret :any}}}}}]
#?(:clj
(is (= expected-out (cljk-collect-for-test))))
#?(:cljs
(is (= expected-out (cljk-collect-for-test)))))
(testing "sequential elements"
(is (= :seqable
(clj-kondo/transform [:repeat :int])))
(is (= :seqable
(clj-kondo/transform [:repeat [:map [:price :int]]])))
(is (= :seqable
(clj-kondo/transform [:repeat [:tuple :int]]))))
(testing "regular expressions"
(is (= :string (clj-kondo/transform [:re "kikka"]))
"the :re schema models a string, clj-kondo's :regex a Pattern object")))
#?(:clj
(deftest fix-1083
(clj-kondo/emit! {:key "value"})
(let [data (edn/read-string (slurp (io/file ".clj-kondo/imports/metosin/malli-types-clj/config.edn")))]
(is (map? data))
(is (= [:linters] (keys data))))))
+3699
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
(ns malli.demo
(:require [malli.core :as m]
[malli.dev :as dev]
[malli.experimental :as mx]))
;; via var metadata
(defn kikka
{:malli/schema [:-> :int :int]}
[x] (inc x))
;; external malli definition
(m/=> kukka [:-> :int :int])
(defn kukka [x]
(inc x))
;; inline schemas (plumatic-style)
(mx/defn kakka :- :int [x :- :int]
(inc x))
(comment
(dev/start!)
(dev/stop!))
(comment
(kikka "1")
(kukka "1")
(kakka "1"))
+391
View File
@@ -0,0 +1,391 @@
(ns malli.destructure-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.destructure :as md]))
(def expectations
[{:name "empty"
:bind '[]
:schema :cat}
{:name "1 arg"
:bind '[a]
:schema [:cat :any]}
{:name "2 args"
:bind '[a b]
:schema [:cat :any :any]}
{:name "2 + varargs"
:bind '[a b & cs]
:schema [:cat :any :any [:* :any]]}
{:name "sequence destructuring"
:bind '[a [b1 [b2] & bs :as bss] & [c1 c2 & cs :as css]]
:schema [:cat
:any
[:maybe
[:cat
[:? :any]
[:maybe
[:cat
[:? :any]
[:* :any]]]
[:* :any]]]
[:maybe
[:cat
[:? :any]
[:? :any]
[:* :any]]]]}
{:name "map destructuring"
:bind '[a {:keys [b]
:strs [c]
:syms [d]
:demo/syms [e]
:demo/keys [f]
g :demo/g
h 123
:or {b 0, d 0, f 0}
:as map}]
:schema [:cat
:any
[:orn
;; Unfortunately, the output order is different between clj and cljs, and we use strict equality in the test
[:map #?(:clj
[:map
[:b {:optional true} :any]
["c" {:optional true} :any]
['d {:optional true} :any]
['demo/e {:optional true} :any]
[:demo/f {:optional true}]
[123 {:optional true} :any]
[:demo/g {:optional true}]]
:cljs
[:map
[:b {:optional true} :any]
["c" {:optional true} :any]
['d {:optional true} :any]
['demo/e {:optional true} :any]
[:demo/f {:optional true}]
[:demo/g {:optional true}]
[123 {:optional true} :any]])]
[:args [:schema
#?(:clj
[:*
[:alt
[:cat [:= :b] :any]
[:cat [:= "c"] :any]
[:cat [:= 'd] :any]
[:cat [:= 'demo/e] :any]
[:cat [:= :demo/f] :demo/f]
[:cat [:= 123] :any]
[:cat [:= :demo/g] :demo/g]
[:cat [:not [:enum :b "c" 'd 'demo/e :demo/f 123 :demo/g]] :any]]]
:cljs
[:*
[:alt
[:cat [:= :b] :any]
[:cat [:= "c"] :any]
[:cat [:= 'd] :any]
[:cat [:= 'demo/e] :any]
[:cat [:= :demo/f] :demo/f]
[:cat [:= :demo/g] :demo/g]
[:cat [:= 123] :any]
[:cat [:not [:enum :b "c" 'd 'demo/e :demo/f :demo/g 123]] :any]]])]]]]
:errors '[[{::keysz [z]}]
[{:kikka/keyz [z]}]]}
{:name "map destructuring with required-keys"
:bind '[{:keys [a :demo/b] :demo/keys [c]}]
:options {::md/required-keys true}
:schema [:cat
[:orn
[:map [:map
[:a :any]
:demo/b
:demo/c]]
[:args [:schema [:* [:alt
[:cat [:= :a] :any]
[:cat [:= :demo/b] :demo/b]
[:cat [:= :demo/c] :demo/c]
[:cat [:not [:enum :a :demo/b :demo/c]] :any]]]]]]]}
{:name "map destructuring with required-keys and closed-maps"
:bind '[{:keys [a :demo/b] :demo/keys [c]}]
:options {::md/required-keys true
::md/closed-maps true}
:schema [:cat
[:orn
[:map [:map {:closed true}
[:a :any]
:demo/b
:demo/c]]
[:args [:schema [:* [:alt
[:cat [:= :a] :any]
[:cat [:= :demo/b] :demo/b]
[:cat [:= :demo/c] :demo/c]]]]]]]}
{:name "map destructuring with required-keys, closed-maps and references disallowed"
:bind '[{:keys [a :demo/b] :demo/keys [c]}]
:options {::md/required-keys true
::md/closed-maps true
::md/references false}
:schema [:cat
[:orn
[:map [:map {:closed true}
[:a :any]
[:demo/b :any]
[:demo/c :any]]]
[:args [:schema [:* [:alt
[:cat [:= :a] :any]
[:cat [:= :demo/b] :any]
[:cat [:= :demo/c] :any]]]]]]]}
{:name "map destructuring with required-keys, closed-maps, references and no sequential-maps"
:bind '[{:keys [a :demo/b] :demo/keys [c]}]
:options {::md/required-keys true
::md/closed-maps true
::md/sequential-maps false}
:schema [:cat
[:map {:closed true}
[:a :any]
:demo/b
:demo/c]]}
{:name "Keyword argument functions now also accept maps"
:bind '[a & {:keys [b]
:strs [c]
:syms [d]
:demo/keys [e]
:demo/syms [f]
:or {b 0, d 0, f 0} :as map}]
:options {::md/sequential-maps false} ;; no effect here
:schema [:cat
:any
[:orn
[:map [:map
[:b {:optional true} :any]
["c" {:optional true} :any]
['d {:optional true} :any]
[:demo/e {:optional true}]
['demo/f {:optional true} :any]]]
[:args [:*
[:alt
[:cat [:= :b] :any]
[:cat [:= "c"] :any]
[:cat [:= 'd] :any]
[:cat [:= :demo/e] :demo/e]
[:cat [:= 'demo/f] :any]
[:cat [:not [:enum :b "c" 'd :demo/e 'demo/f]] :any]]]]]]}
{:name "Nested Keyword argument"
:bind '[[& {:keys [a b] :as opts}]
& {:keys [a b] :as opts}]
:schema [:cat
[:maybe
[:cat
[:orn
[:map [:map
[:a {:optional true} :any]
[:b {:optional true} :any]]]
[:args [:* [:alt
[:cat [:= :a] :any]
[:cat [:= :b] :any]
[:cat [:not [:enum :a :b]] :any]]]]]]]
[:orn
[:map [:map
[:a {:optional true} :any]
[:b {:optional true} :any]]]
[:args [:* [:alt
[:cat [:= :a] :any]
[:cat [:= :b] :any]
[:cat [:not [:enum :a :b]] :any]]]]]]}
{:name "Nest right-to-left map syntax"
:bind '[{{inner :inner} :outer}]
:schema [:cat
[:orn
[:map [:map
[:outer
{:optional true}
[:orn
[:map [:map
[:inner {:optional true} :any]]]
[:args [:schema
[:* [:alt
[:cat [:= :inner] :any]
[:cat [:not [:enum :inner]] :any]]]]]]]]]
[:args [:schema
[:* [:alt
[:cat
[:= :outer]
[:orn
[:map [:map [:inner {:optional true} :any]]]
[:args [:schema [:* [:alt
[:cat [:= :inner] :any]
[:cat [:not [:enum :inner]] :any]]]]]]]
[:cat [:not [:enum :outer]] :any]]]]]]]}])
(def schematized-expectations
[{:name "empty"
:bind '[]
:schema :cat}
{:name "1 arg"
:bind '[a :- :int]
:schema [:cat :int]}
{:name "2 args"
:bind '[a :- :int, b :- :boolean]
:schema [:cat :int :boolean]}
{:name "2 + varargs"
:bind '[a, b :- :int & cs :- [:* :boolean]]
:schema [:cat :any :int [:* :boolean]]}
{:name "Sequence destructuring - 1"
:bind '[a :- :int [b1 :- :int [b2 :- :int] & bs :as bss]]
:schema [:cat
:int
[:maybe
[:cat
[:? :int]
[:maybe
[:cat
[:? :int]
[:* :any]]]
[:* :any]]]]}
{:name "Sequence destructuring - 2 (rest)"
:bind '[a :- :int [b1 :- :int [b2 :- :int] & bs :- [:* :int] :as bss]]
:schema [:cat
:int
[:maybe
[:cat
[:? :int]
[:maybe
[:cat
[:? :int]
[:* :any]]]
[:* :int]]]]}
{:name "Sequence destructuring - 3 (as)"
:bind '[a :- :int [b1 :- :int [b2 :- :int] & bs :as bss :- [:* :int]]]
:schema [:cat
:int
[:schema [:* :int]]]}
{:name "Sequence destructuring - 4 (bind rest)"
:bind '[a :- :int & [b1 :- :int [b2 :- :int] & bs :- [:* :int] :as bss]]
:schema [:cat
:int
[:maybe
[:cat
[:? :int]
[:maybe
[:cat
[:? :int]
[:* :any]]]
[:* :int]]]]}
{:name "map destructuring"
:bind '[a :- :int, {:keys [b]
:strs [c]
:syms [d]
:demo/keys [e]
:demo/syms [f]
:or {b 0, d 0, f 0} :as map}
:- [:map
[:b :int]
["c" :int]
[d :string]
[:demo/e :string]
[demo/f :symbol]]]
:schema [:cat
:int
[:map
[:b :int]
["c" :int]
['d :string]
[:demo/e :string]
['demo/f :symbol]]]}
{:name "Keyword argument functions now also accept maps"
:bind '[& {:keys [b]
:strs [c]
:syms [d]
:demo/keys [e]
:demo/syms [f]
:or {b 0, d 0, f 0} :as map}
:- [:map
[:b :int]
["c" :int]
[d :string]
[:demo/e :string]
[demo/f :symbol]]]
:schema [:cat
[:map
[:b :int]
["c" :int]
['d :string]
[:demo/e :string]
['demo/f :symbol]]]}
{:name "Nested Keyword argument"
:bind '[[& {:keys [a b] :as opts} :- [:map [:a :int] [:b :int]]]
& {:keys [a b] :as opts} :- [:map [:a :int] [:b :int]]]
:schema [:cat
[:maybe
[:cat
[:map
[:a :int]
[:b :int]]]]
[:map
[:a :int]
[:b :int]]]}
{:name "derived map keys"
:bind '[{[g :- :int & gs :- [:* :string]] :value
[a & as :as aas :- [:* :boolean]] 123}]
:options {::md/sequential-maps false
::md/required-keys true}
:schema [:cat [:map
[:value [:maybe [:cat
[:? :int]
[:* :string]]]]
[123 [:schema [:* :boolean]]]]]}])
(deftest parse-test
(let [test-all (fn [expectations]
(doseq [{:keys [name bind errors options] expected :schema} expectations]
(testing (str "- " name " -")
(let [{:keys [arglist schema]} (md/parse bind options)]
(testing "has expected schema"
(when-not (is (= expected schema))
(prn "?" expected)
(prn ">" schema)))
(testing "has valid arglist"
(is (not= ::m/invalid arglist)))
(testing "errors"
(doseq [error errors]
(is (thrown? #?(:clj Exception, :cljs js/Error) (md/parse error)))))))))]
(testing "parsing schematized syntax"
(let [syntax '[x :- :int]]
(testing "succeeds by default"
(is (= [:cat :int] (:schema (md/parse syntax)))))
(testing "fails if inline-schemas is disables"
(is (thrown? #?(:clj Exception, :cljs js/Error) (md/parse syntax {::md/inline-schemas false}))))))
(testing "vanilla clojure"
(test-all expectations))
(testing "schematized clojure"
(test-all schematized-expectations))))
(deftest binding-schema
(is (m/form md/Binding)))
(deftest function-schema-test
(is (= [:=> [:cat [:map [:a :any] :demo/b :demo/c]] :any]
(md/-function-schema
'[[{:keys [a :demo/b] :demo/keys [c]}]]
{::md/sequential-maps false
::md/required-keys true})))
(is (= [:function
[:=> [:cat :int] :any]
[:=> [:cat :int [:* :int]] :any]]
(md/-function-schema
'([a :- :int]
[a :- :int & bs :- [:* :int]])))))
(defn my-var
([a] (my-var a nil))
([a & bs] [a bs]))
#?(:clj
(deftest infer-test
(is (= [:function
[:=> [:cat :any] :any]
[:=> [:cat :any [:* :any]] :any]]
(md/infer #'my-var)))))
+32
View File
@@ -0,0 +1,32 @@
(ns malli.dev.cljs-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.dev.cljs :as md]
[malli.instrument :as mi]
[malli.dev.pretty :as pretty]))
(defn plus
{:malli/schema [:=> [:cat :int] [:int {:max 6}]]}
[x] (inc x))
(defn ->plus [] plus)
(deftest ^:simple start!-test
(testing "malli.dev.cljs/start!"
(testing "without starting"
(is (= "21" ((->plus) "2")))
(is (= 7 ((->plus) 6))))
(testing "instrumentation after starting"
(md/start! {:ns 'malli.dev.cljs-test :filters [(mi/-filter-ns 'malli.dev.cljs-test)]})
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" ((->plus) "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" ((->plus) 6)))
(m/-deregister-metadata-function-schemas! :cljs)
(mi/unstrument! {:filters [(mi/-filter-ns 'malli.dev.cljs-test)]}))
(testing "instrumentation after starting with reporter"
(md/start! {:ns 'malli.dev.cljs-test :report (pretty/thrower) :filters [(mi/-filter-ns 'malli.dev.cljs-test)]})
(is (thrown-with-msg? js/Error #"Invalid function arguments" ((->plus) "2")))
(is (thrown-with-msg? js/Error #"Invalid function return value" ((->plus) 6)))
(m/-deregister-metadata-function-schemas! :cljs)
(mi/unstrument! {:filters [(mi/-filter-ns 'malli.dev.cljs-test)]}))))
+18
View File
@@ -0,0 +1,18 @@
(ns malli.dev.pretty-test
(:require [clojure.test :refer [deftest is]]
[malli.core-test :as mct]
[malli.dev.pretty :as pretty]))
(deftest explain-test
(is (nil? (pretty/explain :string "1")))
(is (re-find
#"Validation Error"
(with-out-str
(is (mct/results= {:schema :string
:value 1
:errors [{:path []
:in []
:schema :string
:value 1
:message "should be a string"}]}
(pretty/explain :string 1)))))))
+8
View File
@@ -0,0 +1,8 @@
(ns malli.dev.virhe-test
(:require [clojure.test :refer [deftest is testing]]
[malli.dev.virhe :as virhe]))
(deftest -printer-test
(testing "function values can be printed"
(is (virhe/-visit {:fn inc} (virhe/-printer)))))
+25
View File
@@ -0,0 +1,25 @@
(ns malli.dev-err-test
(:require [clojure.test :refer [deftest is testing]]
[malli.dev :as dev]))
(defn plus-err
[x] (inc x))
(defn ->plus-err [] plus-err)
(deftest start!-err-test
(testing "malli.dev/start!"
(testing "without starting"
(is (thrown? ClassCastException ((->plus-err) "2")))
(is (= 7 ((->plus-err) 6))))
(testing "instrumentation shema error when starting"
;; append metadata only during test to prevent conflicts with other tests
(alter-meta! #'plus-err #(assoc % :malli/schema [:=> [:cat [:vector]] [:int {:max 6}]]))
(try
(is (thrown-with-msg?
Exception #":malli.core/register-function-schema"
(dev/start! {:ns *ns*, :report (fn [& _args])})))
(finally
(with-out-str (dev/stop!))))
(alter-meta! #'plus-err #(dissoc % :malli/schema)))))
+28
View File
@@ -0,0 +1,28 @@
(ns malli.dev-test
(:require [clojure.test :refer [deftest is testing]]
[malli.dev :as md]
[malli.dev.pretty :as pretty]))
(defn plus
{:malli/schema [:=> [:cat :int] [:int {:max 6}]]}
[x] (inc x))
(defn ->plus [] plus)
(deftest start!-test
(testing "malli.dev/start!"
(testing "without starting"
(is (thrown? ClassCastException ((->plus) "2")))
(is (= 7 ((->plus) 6))))
(testing "instrumentation after starting"
(md/start! {:ns *ns*})
(is (thrown-with-msg? Exception #":malli.core/invalid-input" ((->plus) "2")))
(is (thrown-with-msg? Exception #":malli.core/invalid-output" ((->plus) 6)))
(md/stop!))
(testing "instrumentation after starting with reporter"
(md/start! {:ns *ns* :report (pretty/thrower)})
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Invalid function arguments" ((->plus) "2")))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Invalid function return value" ((->plus) 6)))
(md/stop!))))
+162
View File
@@ -0,0 +1,162 @@
(ns malli.distributive-test
(:require [clojure.test :refer [deftest is]]
[malli.core :as m]
[malli.generator :as mg]
[malli.util :as mu]))
(def options {:registry (merge (mu/schemas) (m/default-schemas))})
(defn dist [s]
(m/form (m/deref s options)))
(defn valid? [?schema value] (m/validate ?schema value options))
(deftest distributive-multi-test
(is (= (dist
[:merge
[:map [:x :int]]
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]])
[:multi {:dispatch :y}
[1 [:map [:x :int] [:y [:= 1]]]]
[2 [:map [:x :int] [:y [:= 2]]]]]))
(is (= (dist
[:merge
[:map [:x :int]]
[:schema
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]]])
[:multi {:dispatch :y}
[1 [:map [:x :int] [:y [:= 1]]]]
[2 [:map [:x :int] [:y [:= 2]]]]]))
(is (= (dist
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]])
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]] [:x :int]]]
[2 [:map [:y [:= 2]] [:x :int]]]]))
(is (= (dist
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]
[:map [:z :int]]])
(dist
[:merge
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]]
[:map [:z :int]]])
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]] [:x :int] [:z :int]]]
[2 [:map [:y [:= 2]] [:x :int] [:z :int]]]]))
(is (= (dist
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]
[:map [:z :int]]
[:multi {:dispatch :y}
[3 [:map [:y [:= 3]]]]
[4 [:map [:y [:= 4]]]]]])
(dist
[:merge
[:merge
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]]
[:map [:z :int]]]
[:multi {:dispatch :y}
[3 [:map [:y [:= 3]]]]
[4 [:map [:y [:= 4]]]]]])
[:multi {:dispatch :y}
[1 [:multi {:dispatch :y}
[3 [:map [:y [:= 3]] [:x :int] [:z :int]]]
[4 [:map [:y [:= 4]] [:x :int] [:z :int]]]]]
[2 [:multi {:dispatch :y}
[3 [:map [:y [:= 3]] [:x :int] [:z :int]]]
[4 [:map [:y [:= 4]] [:x :int] [:z :int]]]]]]))
(is (= (dist
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]
[:map [:z :int]]
[:multi {:dispatch :a}
[3 [:map [:a [:= 3]]]]
[4 [:map [:a [:= 4]]]]]])
[:multi {:dispatch :y}
[1 [:multi {:dispatch :a}
[3 [:map [:y [:= 1]] [:x :int] [:z :int] [:a [:= 3]]]]
[4 [:map [:y [:= 1]] [:x :int] [:z :int] [:a [:= 4]]]]]]
[2 [:multi {:dispatch :a}
[3 [:map [:y [:= 2]] [:x :int] [:z :int] [:a [:= 3]]]]
[4 [:map [:y [:= 2]] [:x :int] [:z :int] [:a [:= 4]]]]]]]))
(is (= (dist
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:multi {:dispatch :y}
[3 [:map [:y [:= 3]]]]
[4 [:map [:y [:= 4]]]]]])
[:multi {:dispatch :y}
[1 [:multi {:dispatch :y}
[3 [:map [:y [:= 3]]]]
[4 [:map [:y [:= 4]]]]]]
[2 [:multi {:dispatch :y}
[3 [:map [:y [:= 3]]]]
[4 [:map [:y [:= 4]]]]]]]))
(is (= (dist
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:multi {:dispatch :z}
[3 [:map [:z [:= 3]]]]
[4 [:map [:z [:= 4]]]]]])
[:multi {:dispatch :y}
[1 [:multi {:dispatch :z}
[3 [:map [:y [:= 1]] [:z [:= 3]]]]
[4 [:map [:y [:= 1]] [:z [:= 4]]]]]]
[2 [:multi {:dispatch :z}
[3 [:map [:y [:= 2]] [:z [:= 3]]]]
[4 [:map [:y [:= 2]] [:z [:= 4]]]]]]])))
(deftest parse-distributive-multi-test
(is (= (m/tag 1 (m/tag 3 {:y 1, :z 3}))
(m/parse
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:multi {:dispatch :z}
[3 [:map [:z [:= 3]]]]
[4 [:map [:z [:= 4]]]]]]
{:y 1 :z 3}
options))))
(deftest gen-distributive-multi-test
(is (= [{:y 1, :z 3} {:y 2, :z 4} {:y 2, :z 3} {:y 2, :z 3} {:y 1, :z 4}
{:y 1, :z 3} {:y 1, :z 3} {:y 1, :z 3} {:y 1, :z 3} {:y 2, :z 4}]
(mg/sample
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:multi {:dispatch :z}
[3 [:map [:z [:= 3]]]]
[4 [:map [:z [:= 4]]]]]]
(assoc options :seed 0)))))
+85
View File
@@ -0,0 +1,85 @@
(ns malli.dot-test
(:require [clojure.string :as str]
[clojure.test :refer [deftest is]]
[malli.dot :as md]))
(def Order
[:schema
{:registry {"Country" [:map
[:name [:enum :FI :PO]]
[:neighbors [:vector [:ref "Country"]]]]
"Burger" [:map
[:name string?]
[:description {:optional true} string?]
[:origin [:maybe "Country"]]
[:price pos-int?]]
"OrderLine" [:map
[:burger "Burger"]
[:amount int?]]
"Order" [:map
[:lines [:vector "OrderLine"]]
[:delivery [:map
[:delivered boolean?]
[:address [:map
[:street string?]
[:zip int?]
[:country "Country"]]]]]]}}
"Order"])
(defn trimmed= [s1 s2]
(letfn [(trim [x] (str/trim (str/replace x #"\s+" " ")))]
(= (trim s1) (trim s2))))
(deftest transform-test
(is (trimmed=
"digraph {
node [shape=\"record\", style=\"filled\", color=\"#000000\"]
edge [dir=\"back\", arrowtail=\"none\"]
\":malli.dot/schema\" [label=\"{:malli.dot/schema|[:enum \\{:title \\\"enum\\\"\\} \\\"S\\\" \\\"M\\\" \\\"L\\\"]\\l}\", fillcolor=\"#fff0cd\"]
}"
(md/transform
[:enum {:title "enum"} "S" "M" "L"])))
(is (trimmed=
"digraph {
node [shape=\"record\", style=\"filled\", color=\"#000000\"]
edge [dir=\"back\", arrowtail=\"none\"]
\":malli.dot/schema\" [label=\"{:malli.dot/schema|:x :string\\l}\", fillcolor=\"#fff0cd\"]
}"
(md/transform
[:map {:x 1}
[:x {:x 1} :string]])))
(is (trimmed=
"digraph {
node [shape=\"record\", style=\"filled\", color=\"#000000\"]
edge [dir=\"back\", arrowtail=\"none\"]
\"Burger\" [label=\"{Burger|:name string?\\l:description string?\\l:origin [:maybe \\\"Country\\\"]\\l:price pos-int?\\l}\", fillcolor=\"#fff0cd\"]
\"Country\" [label=\"{Country|:name [:enum :FI :PO]\\l:neighbors [:vector [:ref \\\"Country\\\"]]\\l}\", fillcolor=\"#fff0cd\"]
\"Order\" [label=\"{Order|:lines [:vector \\\"OrderLine\\\"]\\l:delivery Order$Delivery\\l}\", fillcolor=\"#fff0cd\"]
\"Order$Delivery\" [label=\"{Order$Delivery|:delivered boolean?\\l:address Order$Delivery$Address\\l}\", fillcolor=\"#e6caab\"]
\"Order$Delivery$Address\" [label=\"{Order$Delivery$Address|:street string?\\l:zip int?\\l:country Country\\l}\", fillcolor=\"#e6caab\"]
\"OrderLine\" [label=\"{OrderLine|:burger Burger\\l:amount int?\\l}\", fillcolor=\"#fff0cd\"]
\"Burger\" -> \"Country\" [arrowtail=\"odiamond\"]
\"Country\" -> \"Country\" [arrowtail=\"odiamond\"]
\"Order\" -> \"OrderLine\" [arrowtail=\"odiamond\"]
\"Order\" -> \"Order$Delivery\" [arrowtail=\"diamond\"]
\"Order$Delivery\" -> \"Order$Delivery$Address\" [arrowtail=\"diamond\"]
\"Order$Delivery$Address\" -> \"Country\" [arrowtail=\"odiamond\"]
\"OrderLine\" -> \"Burger\" [arrowtail=\"odiamond\"]
}"
(md/transform Order)))
(is (trimmed=
"digraph {
node [shape=\"record\", style=\"filled\", color=\"#000000\"]
edge [dir=\"back\", arrowtail=\"none\"]
\":malli.dot/schema\" [label=\"{:malli.dot/schema|[:and int? [:\\< 100]]\\l}\", fillcolor=\"#fff0cd\"]
}"
(md/transform [:and int? [:< 100]]))))
+945
View File
@@ -0,0 +1,945 @@
(ns malli.error-test
(:require [clojure.test :refer [are deftest is testing]]
[malli.core :as m]
[malli.core-test]
[malli.error :as me]
[malli.generator :as mg]
[malli.util :as mu]
#?(:clj [malli.test-macros :refer [when-env]]))
#?(:cljs (:require-macros [malli.test-macros :refer [when-env]]))
#?(:cljs (:import (goog Uri))))
(deftest error-message-test
(let [msg "should be an int"
fn1 (fn [{:keys [value]} _] (str "should be an int, was " value))
fn2 '(fn [{:keys [value]} _] (str "should be an int, was " value))]
(doseq [[schema value message opts]
[;; via schema
[[int? {:error/message msg}] "kikka" "should be an int"]
[[int? {:error/fn fn1}] "kikka" "should be an int, was kikka"]
[[int? {:error/fn fn2}] "kikka" "should be an int, was kikka"]
[[int? {:error/message msg, :error/fn fn2}] "kikka" "should be an int, was kikka"]
;; via defaults
[[int?] "kikka" "should be an int" {:errors {'int? {:error/message msg}}}]
[[int?] "kikka" "should be an int, was kikka" {:errors {'int? {:error/fn fn1}}}]
[[int?] "kikka" "should be an int, was kikka" {:errors {'int? {:error/fn fn2}}}]
[[int?] "kikka" "should be an int, was kikka" {:errors {'int? {:error/message msg, :error/fn fn2}}}]
;; both
[[int?
{:error/message msg, :error/fn fn2}]
"kikka" "should be an int, was kikka"
{:errors {'int? {:error/message "fail1", :error/fn (constantly "fail2")}}}]
;; type-properties
[malli.core-test/Over6 5 "should be over 6"]]]
(is (= message (-> (m/explain schema value) :errors first (me/error-message opts)))))))
(deftest with-spell-checking-test
(let [get-errors (fn [explanation] (->> explanation :errors (mapv #(select-keys % [:path :type ::me/likely-misspelling-of :message]))))]
(testing "simple"
(is (= [{:path [:deliverz]
:type ::me/misspelled-key
::me/likely-misspelling-of [[:deliver]]
:message "should be spelled :deliver"}]
(-> [:map
[:orders boolean?]
[:deliver boolean?]]
(mu/closed-schema)
(m/explain {:orders true, :deliverz true})
(me/with-spell-checking)
(me/with-error-messages)
(get-errors))))
(is (= [{:path ["deliverz"]
:type ::me/misspelled-key
::me/likely-misspelling-of [["deliver"]]
:message #?(:clj "should be spelled \"deliver\""
:cljs "should be spelled deliver")}]
(-> [:map
["orders" boolean?]
["deliver" boolean?]]
(mu/closed-schema)
(m/explain {"orders" true, "deliverz" true})
(me/with-spell-checking)
(me/with-error-messages)
(get-errors)))))
(testing "nested"
(testing "with defaults"
(is (= [{:path [0 :address 0 :streetz]
:type ::me/misspelled-key
::me/likely-misspelling-of [[0 :address 0 :street1] [0 :address 0 :street2]],
:message "should be spelled :street1 or :street2"}]
(-> [:maybe [:map
[:address [:and
[:map
[:street1 string?]
[:street2 string?]]]]]]
(mu/closed-schema)
(m/explain {:address {:streetz "123"}})
(me/with-spell-checking)
(me/with-error-messages)
(get-errors)))))
(testing "stripping likely-misspelled-of fields"
(is (= [{:path [:address :street1]
:type ::m/missing-key
:message "missing required key"}
{:path [:address :street2]
:type ::m/missing-key
:message "missing required key"}
{:path [:address :streetz]
:type ::me/misspelled-key
::me/likely-misspelling-of [[:address :street1] [:address :street2]]
:message "should be spelled :street1 or :street2"}]
(-> [:map
[:address [:map
[:street1 string?]
[:street2 string?]]]]
(mu/closed-schema)
(m/explain {:address {:streetz "123"}})
(me/with-spell-checking {:keep-likely-misspelled-of true})
(me/with-error-messages)
(get-errors))))))))
(deftest humanize-test
(testing "nil if success"
(is (nil? (-> int?
(m/explain 1)
(me/humanize)))))
(testing "top-level error"
(is (= ["should be an int"]
(-> int?
(m/explain "1")
(me/humanize)))))
(testing "vector"
(is (= [nil nil [nil ["should be an int"]]]
(-> [:vector [:vector int?]]
(m/explain [[1 2] [2 2] [3 "4"]])
(me/humanize)))))
(testing "set"
(is (= #{#{["should be a keyword"]}}
(-> [:set [:set keyword?]]
(m/explain #{#{42 :a {}}})
(me/humanize)))))
(testing "invalid type"
(is (= ["invalid type"]
(-> [:set int?]
(m/explain [1])
(me/humanize)))))
(testing "mixed bag"
(is (= [nil
{:x [nil ["should be an int"] ["should be an int"]]}
{:x ["invalid type"]}]
(-> [:vector [:map [:x [:vector int?]]]]
(m/explain
[{:x [1 2 3]}
{:x [1 "2" "3"]}
{:x #{"whatever"}}])
(me/humanize)))))
(testing "so nested"
(is (= {:data [{:x [["should be an int"] nil ["should be an int"]]}
{:x [["should be an int"] nil ["should be an int"]]}
nil
{:x [["should be an int"]]}]}
(-> [:map [:data [:vector [:map [:x [:vector int?]]]]]]
(m/explain
{:data [{:x ["1" 2 "3"]} {:x ["1" 2 "3"]} {:x [1]} {:x ["1"]} {:x [1]}]})
(me/humanize)))))
(testing "disallowed keys in closed maps"
(is (= {:extra ["disallowed key"]}
(-> [:map {:closed true} [:x int?]]
(m/explain {:x 1, :extra "key"})
(me/humanize)))))
(testing "multiple errors on same key are preserved"
(is (= {:x ["missing required key" "missing required key"]}
(me/humanize
{:value {},
:errors [{:in [:x], :schema [:map [:x int?]], :type ::m/missing-key}
{:in [:x], :schema [:map [:x int?]], :type ::m/missing-key}]}))))
(testing "maps can have top level errors and key errors"
(is (= {:person {:malli/error ["should be a seq"],
:name ["missing required key"]}}
(-> [:map [:person [:and [:map [:name string?]] seq?]]]
(m/explain {:person {}})
(me/humanize)))))
(testing "maps have errors inside"
(is (= {:person ["should be a seq"]}
(-> [:map [:person seq?]]
(m/explain {:person {}})
(me/humanize))))))
(deftest humanize-customization-test
(let [schema [:map
[:a int?]
[:b pos-int?]
[:c [pos-int? {:error/message "STAY POSITIVE"
:error/fn {:fi '(constantly "POSITIIVINEN")}}]]
[:d
[:map
[:e any?]
[:f [int? {:error/message {:en "SHOULD BE ZIP", :fi "PITÄISI OLLA NUMERO"}}]]]]]
value {:a "invalid"
:b "invalid"
:c "invalid"
:d {:f "invalid"}}]
(testing "with default locale"
(is (= {:a ["should be an int"]
:b ["should be a positive int"]
:c ["STAY POSITIVE"],
:d {:e ["missing required key"]
:f ["SHOULD BE ZIP"]}}
(-> (m/explain schema value)
(me/humanize)))))
(testing "localization is applied, if available"
(is (= {:a ["NUMERO"]
:b ["should be a positive int"]
:c ["POSITIIVINEN"],
:d {:e ["PUUTTUVA AVAIN"]
:f ["PITÄISI OLLA NUMERO"]}}
(-> (m/explain schema value)
(me/humanize
{:locale :fi
:errors (-> me/default-errors
(assoc-in ['int? :error/message :fi] "NUMERO")
(assoc-in [::m/missing-key :error/message :fi] "PUUTTUVA AVAIN"))})))))))
(when-env
"TEST_SCI"
(deftest sci-not-available-test
(testing "sci not available"
(let [schema (m/schema [:string {:error/fn '(constantly "FAIL")}] {::m/disable-sci true})]
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli.core/sci-not-available"
(-> schema (m/explain ::invalid) (me/with-error-messages))))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli.core/sci-not-available"
(-> schema (m/explain ::invalid) (me/humanize))))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli.core/sci-not-available"
(-> [:string {:error/fn '(constantly "FAIL")}]
(m/explain ::invalid)
(me/with-error-messages {::m/disable-sci true}))))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli.core/sci-not-available"
(-> [:string {:error/fn '(constantly "FAIL")}]
(m/explain ::invalid)
(me/humanize {::m/disable-sci true}))))
(testing "direct options win"
(is (-> schema (m/explain ::invalid) (me/with-error-messages {::m/disable-sci false})))
(is (-> schema (m/explain ::invalid) (me/humanize {::m/disable-sci false}))))))))
(deftest composing-with-and-test
(testing "top-level map-schemas are written in :malli/error"
(let [schema [:and [:map
[:x int?]
[:y int?]
[:z int?]]
[:fn {:error/message "(> x y)"}
'(fn [{:keys [x y]}] (> x y))]]]
(is (= {:z ["should be an int"], :malli/error ["(> x y)"]}
(-> schema
(m/explain {:x 1 :y 2, :z "1"})
(me/humanize)))))
(testing ":error/path contributes to path"
(let [schema [:and [:map
[:password string?]
[:password2 string?]]
[:fn {:error/message "passwords don't match"
:error/path [:password2]}
'(fn [{:keys [password password2]}]
(= password password2))]]]
(is (= {:password2 ["passwords don't match"]}
(-> schema
(m/explain {:password "secret"
:password2 "faarao"})
(me/humanize)))))))
(testing "on collections"
(let [schema [:and
[:vector int?]
[:fn {:error/message "error1"} '(fn [[x]] (pos? x))]
[:fn {:error/message "error2"} '(fn [[x]] (pos? x))]]]
(testing "value errors are reported over extra top-level errpors"
(is (= [nil ["should be an int"]]
(-> schema
(m/explain [-2 "1"])
(me/humanize)))))
(testing "without value errors, all top-level errors are collected"
(is (= ["error1" "error2"]
(-> schema
(m/explain [-2 1])
(me/humanize))))
(is (= ["invalid type" "error1" "error2"]
(-> schema
(m/explain '(-2 "1"))
(me/humanize)))))))
(testing "on non-collections, all errors are collectd"
(let [schema [:and
[:fn {:error/message "should be >= 1"} '(fn [x] (or (not (int? x)) (>= x 1)))]
int?
[:fn {:error/message "should be >= 2"} '(fn [x] (or (not (int? x)) (>= x 2)))]]]
(is (= ["should be >= 1" "should be >= 2"]
(-> schema
(m/explain 0)
(me/humanize))))
(is (= ["should be an int"]
(-> schema
(m/explain "kikka")
(me/humanize))))
(is (= ["should be >= 2"]
(-> schema
(m/explain 1)
(me/humanize))))
(is (= nil
(-> schema
(m/explain 2)
(me/humanize)))))))
(deftest string-test
(is (= {:a ["should be a string"],
:b ["should be at least 1 character"],
:c ["should be at most 4 characters"],
:d [["should be at least 1 character"]
["should be at most 4 characters"]],
:e ["should be a string"]
:f ["should be 4 characters"]
:g ["should be at most 1 character"]
:h ["should be 1 character"]}
(-> [:map
[:a :string]
[:b [:string {:min 1}]]
[:c [:string {:max 4}]]
[:d [:vector [:string {:min 1, :max 4}]]]
[:e [:string {:min 1, :max 4}]]
[:f [:string {:min 4, :max 4}]]
[:g [:string {:max 1}]]
[:h [:string {:min 1 :max 1}]]]
(m/explain
{:a 123
:b ""
:c "invalid"
:d ["" "12345"]
:e 123
:f "invalid"
:g "ab"
:h ""})
(me/humanize)))))
(deftest int-test
(is (= {:a ["should be an integer"]
:b ["should be at least 1"]
:c ["should be at most 4"]
:d [["should be at least 1"]
["should be at most 4"]]
:e ["should be an integer"]
:f ["should be 4"]}
(-> [:map
[:a :int]
[:b [:int {:min 1}]]
[:c [:int {:max 4}]]
[:d [:vector [:int {:min 1, :max 4}]]]
[:e [:int {:min 1, :max 4}]]
[:f [:int {:min 4, :max 4}]]]
(m/explain
{:a "123"
:b 0
:c 5
:d [0 5]
:e "123"
:f 5})
(me/humanize)))))
(deftest double-test
(doseq [t [:double :float]]
(is (= {:a [(str "should be a " (name t))]
:b ["should be at least 1"]
:c ["should be at most 4"]
:d [["should be at least 1"]
["should be at most 4"]]
:e [(str "should be a " (name t))]
:f ["should be 4"]}
(-> [:map
[:a t]
[:b [t {:min 1}]]
[:c [t {:max 4}]]
[:d [:vector [t {:min 1, :max 4}]]]
[:e [t {:min 1, :max 4}]]
[:f [t {:min 4, :max 4}]]]
(m/explain
{:a "123"
:b 0.0
:c 5.0
:d [0.0 5.0]
:e "123"
:f 5.0})
(me/humanize))))))
(deftest any-test
(testing "success"
(is (= nil
(-> :any
(m/explain "bla")
(me/humanize))))))
(deftest nil-test
(testing "success"
(is (= nil
(-> :nil
(m/explain nil)
(me/humanize)))))
(testing "failure"
(is (= ["should be nil"]
(-> :nil
(m/explain "gogo")
(me/humanize))))))
(deftest re-test
(testing "success"
(is (= nil
(-> [:re #"bla"]
(m/explain "bla")
(me/humanize)))))
(testing "failure"
(is (= ["should match regex"]
(-> [:re "#bla"]
(m/explain "gogo")
(me/humanize))))))
(deftest enum-test
(testing "success"
(is (= nil
(-> [:enum "foo" "bar"]
(m/explain "foo")
(me/humanize)))))
(testing "error with 1 value"
(is (= [#?(:clj "should be \"foo\""
:cljs "should be foo")]
(-> [:enum "foo"]
(m/explain "baz")
(me/humanize)))))
(testing "error with 2 values"
(is (= [#?(:clj "should be either \"foo\" or \"bar\""
:cljs "should be either foo or bar")]
(-> [:enum "foo" "bar"]
(m/explain "baz")
(me/humanize)))))
(testing "more than 2 values"
(is (= [#?(:clj "should be either \"foo\", \"bar\", bar or \"buzz\""
:cljs "should be either foo, bar, bar or buzz")]
(-> [:enum "foo" "bar" 'bar "buzz"]
(m/explain "baz")
(me/humanize))))
(is (= [#?(:clj "should be either \"foo\", \"bar\", \"buzz\" or \"biff\""
:cljs "should be either foo, bar, buzz or biff")]
(-> [:enum "foo" "bar" "buzz" "biff"]
(m/explain "baz")
(me/humanize))))))
(deftest function-test
(is (= ["should be a valid function"]
(-> [:=> [:cat int? int?] int?]
(m/explain malli.core-test/single-arity {::m/function-checker mg/function-checker})
(me/humanize))))
(is (= ["should be a valid function"]
(-> [:=> [:cat int? int?] int?]
(m/explain 123)
(me/humanize)))))
(deftest ifn-test
(is (= ["should be an ifn"]
(me/humanize (m/explain ifn? 123)))))
(defrecord Horror [])
(deftest multi-error-test
(let [schema [:multi {:dispatch :type}
["plus" [:map [:value int?]]]
["minus" [:map [:value int?]]]
['minus [:map [:value int?]]]]]
(is (= {:type ["invalid dispatch value"]}
(-> schema
(m/explain {:type "minuz"})
(me/humanize))))
(is (= {:type [#?(:clj "did you mean \"minus\" or minus"
:cljs "did you mean minus or minus")]}
(-> schema
(m/explain {:type "minuz"})
(me/with-spell-checking)
(me/humanize)))))
(testing "explain works even when dispatch is a keyword but value is not a map"
(is (= ["invalid dispatch value"]
(-> (m/schema [:multi {:dispatch :x}
[:y [:map [:x :keyword]]]])
(m/explain [])
(me/humanize))))
(is (= {:x ["invalid dispatch value"]}
(-> (m/schema [:multi {:dispatch :x}
[:y [:map [:x :keyword]]]])
(m/explain (map->Horror {:foo :bar}))
(me/humanize))))))
(deftest explain-sequential
(is (= [{:x ["missing required key"]}
{:x ["missing required key"]}]
(-> (m/explain
[:sequential [:map [:x [:sequential [:map [:y number?]]]]]]
'({:a 10}
{:b 10}))
(me/humanize)))))
(deftest util-schemas-test
(let [registry (merge (m/default-schemas) (mu/schemas))]
(doseq [[schema errors] [[[:merge {:title "merge"}
[:map [:x int?] [:y int?]]
[:map [:z int?]]]
{:y ["missing required key"]
:z ["missing required key"]}]
[[:union {:title "union"}
[:map [:x int?] [:y int?]]
[:map [:x string?]]]
{:y ["missing required key"]}]
[[:select-keys {:title "select-keys"}
[:map [:x int?] [:y int?]]
[:x]]]]
:let [schema (m/schema schema {:registry registry})]]
(is (= errors (-> schema (m/explain {:x 1}) (me/humanize)))))))
(deftest sequence-test
(is (= [nil ["end of input"]]
(-> [:cat int? int?]
(m/explain [1])
(me/humanize))))
(is (= [nil nil ["input remaining"]]
(-> [:cat int? int?]
(m/explain [1 2 3])
(me/humanize))))
(is (= [nil nil ["should be an int" "should be a string" "input remaining"]]
(-> [:cat int? int? [:? int?] [:? string?]]
(m/explain [1 2 :foo])
(me/humanize)))))
(def VarSchema [:map [:foo :int]])
(deftest error-definion-lookup-test
(is (= {:foo ["should be an integer"]}
(-> [:map
[:foo :int]]
(m/explain {:foo "1"})
(me/humanize {:resolve me/-resolve-root-error}))))
(is (= {:foo ["entry-failure"]}
(-> [:map
[:foo {:error/message "entry-failure"} :int]]
(m/explain {:foo "1"})
(me/humanize {:resolve me/-resolve-root-error}))))
(is (= ["map-failure"]
(-> [:map {:error/message "map-failure"}
[:foo {:error/message "entry-failure"} :int]]
(m/explain {:foo "1"})
(me/humanize {:resolve me/-resolve-root-error}))))
(testing "entry sees child schema via :error/fn"
(is (= {:foo ["failure"]}
(-> [:map
[:foo {:error/fn (fn [{:keys [schema]} _]
(-> schema m/properties :reason))} [:int {:reason "failure"}]]]
(m/explain {:foo "1"})
(me/humanize {:resolve me/-resolve-root-error})))))
(testing "enum #553"
(is (= {:a [#?(:clj "should be either \"a\", \"b\", a or b"
:cljs "should be either a, b, a or b")]}
(-> [:map
[:a [:enum "a" "b" 'a 'b]]]
(m/explain {:a nil})
(me/humanize {:resolve me/-resolve-root-error})))))
(testing "find over non-maps"
(is (= [["should be an integer"]]
(-> [:sequential [:and :int]]
(m/explain [1 "2"])
(me/humanize {:resolve me/-resolve-root-error})))))
(testing "correct paths"
(is (= ["should be an integer" "should be an integer" "should be an integer"]
(me/humanize
(m/explain [:and [:and :int :int :int]] "2")
{:resolve me/-resolve-direct-error})
(me/humanize
(m/explain [:and [:and :int :int :int]] "2")
{:resolve me/-resolve-root-error}))))
(testing "collecting all properties"
(are [schema expected]
(let [{:keys [errors] :as error} (m/explain schema {:foo "1"})]
(= [expected] (map #(me/-resolve-root-error error % nil) errors)))
;; direct
[:map [:foo [:int {:error/message "direct-failure" ::level :warn}]]]
[[:foo]
"direct-failure"
{:error/message "direct-failure", ::level :warn}]
;; entry
[:map [:foo {:error/message "entry-failure" ::level :warn} :int]]
[[:foo]
"entry-failure"
{:error/message "entry-failure", ::level :warn}]
;; one up
[:map {:error/message "map-failure" ::level :warn} [:foo :int]]
[[]
"map-failure"
{:error/message "map-failure", ::level :warn}]))
(testing ":fn with :error/path #554"
(is (= {:password2 ["passwords don't match"]}
(-> [:and [:map
[:password string?]
[:password2 string?]]
[:fn {:error/message "passwords don't match"
:error/path [:password2]}
'(fn [{:keys [password password2]}]
(= password password2))]]
(m/explain {:password "secret"
:password2 "faarao"})
(me/humanize {:resolve me/-resolve-root-error})))))
(testing "refs #1106"
(is (= {:foo ["should be an integer"]}
(me/humanize
(m/explain [:ref #'VarSchema] {:foo "2"})
{:resolve me/-resolve-direct-error})))
(is (= {:foo ["should be an integer"]}
(me/humanize
(m/explain [:ref #'VarSchema] {:foo "2"})
{:resolve me/-resolve-root-error})))))
(deftest limits
(is (= {:a [["should be an int"]]
:b ["should have at least 2 elements"]
:c ["should have at most 5 elements"]
:d [["should have at least 2 elements"]
["should have at most 5 elements"]]
:e ["should have at least 2 elements"]
:f ["should have 5 elements"]}
(-> [:map
[:a [:vector int?]]
[:b [:vector {:min 2} int?]]
[:c [:vector {:max 5} int?]]
[:d [:vector [:vector {:min 2, :max 5} int?]]]
[:e [:vector {:min 2, :max 5} int?]]
[:f [:vector {:min 5, :max 5} int?]]]
(m/explain
{:a ["123"]
:b [1]
:c [1 2 3 4 5 6]
:d [[1] [1 2 3 4 5 6 7]]
:e [1.2]
:f [1 2 3 4]})
(me/humanize)))))
(deftest robust-humanize-form
(let [f (fn [s] [:fn {:error/message s} (constantly false)])
=> ::irrelevant]
(are [schema value _ expected]
(= expected (-> (m/explain schema value) (me/humanize)))
;; simple cases
:any :any => nil
[:and :any :any] :any => nil
[:and (f "1") :any] :any => ["1"]
[:and (f "1") (f "1") :any] :any => ["1" "1"]
[:and (f "1") (f "2")] {:a :map} => ["1" "2"]
;; accumulate into maps if error shape is already a map
[:map [:x [:and [:map [:y :any]] seq?]]] 123 => ["invalid type"]
[:map [:x [:and [:map [:y :any]] seq?]]] {} => {:x ["missing required key"]}
[:map [:x [:and [:map [:y :any]] seq?]]] {:x 123} => {:x ["invalid type" "should be a seq"]}
[:map [:x [:and [:map [:y :any]] seq? (f "kosh")]]] {:x {}} => {:x {:y ["missing required key"]
:malli/error ["should be a seq" "kosh"]}}
[:map [:x [:and [:map [:y :any]] seq?]]] {:x {:y 123}} => {:x ["should be a seq"]}
;; records
[:map [:x [:and [:map [:y :any]] seq?]]] (map->Horror {:x (map->Horror {})}) => {:x {:y ["missing required key"]
:malli/error ["should be a seq"]}}
;; don't derive error form from value in case of top-level error
[:map [:x [:and seq? [:map [:y :any]]]]] 123 => ["invalid type"]
[:map [:x [:and seq? [:map [:y :any]]]]] {} => {:x ["missing required key"]}
[:map [:x [:and seq? [:map [:y :any]]]]] {:x 123} => {:x ["should be a seq" "invalid type"]}
[:map [:x [:and seq? [:map [:y :any]]]]] {:x {}} => {:x ["should be a seq"]}
;; tuple sizes
[:map [:x [:tuple :int :int :int]]] {} => {:x ["missing required key"]}
[:map [:x [:tuple :int :int :int]]] {:x []} => {:x ["invalid tuple size 0, expected 3"]}
[:map [:x [:tuple :int :int :int]]] {:x [1, 2]} => {:x ["invalid tuple size 2, expected 3"]}
[:map [:x [:tuple :int :int :int]]] {:x [1 "2" 3]} => {:x [nil ["should be an integer"]]}
[:map [:x [:tuple :int :int :int]]] {:x [1 "2" "3"]} => {:x [nil ["should be an integer"] ["should be an integer"]]}
[:map [:x [:tuple :int [:and :int (f "fails")] :int]]] {:x [1 "2" "3"]} => {:x [nil ["should be an integer" "fails"] ["should be an integer"]]}
[:map [:x [:tuple :int :int :int]]] {:x [1 2 3]} => nil
;; sequences
[:and [:sequential :int] (f "1") (f "2")] [1 "2"] => [nil ["should be an integer"]]
[:and [:sequential :int] (f "1") (f "2")] [1 2] => ["1" "2"])))
(deftest multi-humanize-test-428
(is (= {:user ["invalid dispatch value"]}
(-> (m/explain [:map [:user [:multi {:dispatch :type}]]] {:user nil})
(me/humanize)))))
(deftest in-error-test
(let [Address [:map {:closed true}
[:id :string]
[:tags [:set :keyword]]
[:numbers [:sequential :int]]
[:address [:map
[:street :string]
[:city :string]
[:zip :int]
[:lonlat [:tuple :double :double]]]]]
address {:id "Lillan"
:EXTRA "KEY"
:tags #{:artesan "coffee" :garden "ground"}
:numbers (list 1 "2" 3 4 "5" 6 7)
:address {:street "Ahlmanintie 29"
:zip 33100
:lonlat [61.4858322, "23.7832851,17"]}}]
(testing "with defaults"
(is (= {:EXTRA "KEY"
:tags #{"coffee" "ground"}
:numbers [nil "2" nil nil "5"]
:address {:lonlat [nil "23.7832851,17"]}}
(-> (m/explain Address address)
(me/error-value)))))
(testing "custom accept"
(is (= {:EXTRA "KEY"
:tags #{"coffee" "ground"}
:numbers [nil "2" nil nil "5"]
:address {:city nil
:lonlat [nil "23.7832851,17"]}}
(-> (m/explain Address address)
(me/error-value {::me/accept-error (constantly true)})))))
(testing "masked valid values"
(let [explain (m/explain Address address)]
(is (= {:id '...
:EXTRA "KEY"
:tags #{"coffee" "ground" '...}
:numbers ['... "2" '... '... "5" '... '...]
:address {:street '...
:zip '...
:lonlat ['... "23.7832851,17"]}}
(me/error-value explain {::me/mask-valid-values '...})))
(is (= [{:EXTRA '..., :address '..., :id '..., :numbers '..., :tags #{"coffee" '...}}
{:EXTRA '..., :address '..., :id '..., :numbers '..., :tags #{"ground" '...}}
{:EXTRA '..., :address '..., :id '... :numbers ['... "2" '... '... '... '... '...], :tags '...}
{:EXTRA '..., :address '..., :id '..., :numbers ['... '... '... '... "5" '... '...], :tags '...}
{:EXTRA '..., :address '..., :id '..., :numbers '..., :tags '...}
{:EXTRA '..., :address {:lonlat ['... "23.7832851,17"], :street '..., :zip '...}, :id '..., :numbers '..., :tags '...}
{:EXTRA "KEY", :address '..., :id '..., :numbers '..., :tags '...}]
(for [error (:errors explain)]
(me/error-value (assoc explain :errors [error]) {::me/mask-valid-values '...}))))))
(testing "masked nested maps #1096"
(is (= {"foo" "foo"}
(-> (m/explain [:map-of :keyword [:map-of :keyword :any]] {"foo" {:bar 1}})
(me/error-value {::me/mask-valid-values '...})))))
(testing "custom painting of errors"
(is (= {:EXTRA {:value "KEY", :type :malli.core/extra-key}
:tags #{{:value "ground"} {:value "coffee"}}
:numbers [nil {:value "2"} nil nil {:value "5"}]
:address {:lonlat [nil {:value "23.7832851,17"}]}}
(-> (m/explain Address address)
(me/error-value {::me/wrap-error #(select-keys % [:value :type])}))))
(testing "keeping valid values"
(is (= {:EXTRA {:type :malli.core/extra-key, :value "KEY"}
:address {:lonlat [61.4858322 {:value "23.7832851,17"}]
:street "Ahlmanintie 29"
:zip 33100}
:id "Lillan"
:numbers [1 {:value "2"} 3 4 {:value "5"} 6 7]
:tags #{:artesan :garden {:value "coffee"} {:value "ground"}}}
(-> (m/explain Address address)
(me/error-value {::me/wrap-error #(select-keys % [:value :type])
::me/keep-valid-values true}))))))))
#?(:clj
(deftest pr-str-humanize-test
(is (= ["should be \"a\""] (me/humanize (m/explain [:enum "a"] 1))))
(is (= ["should be a"] (me/humanize (m/explain [:enum 'a] 1))))
(is (= ["should be either \"a\" or \"b\""] (me/humanize (m/explain [:enum "a" "b"] 1))))
(is (= ["should be either a or b"] (me/humanize (m/explain [:enum 'a 'b] 1))))
(is (= ["should be \"a\""] (me/humanize (m/explain [:= "a"] 1))))
(is (= ["should be a"] (me/humanize (m/explain [:= 'a] 1))))
(is (= ["should not be \"a\""] (me/humanize (m/explain [:not= "a"] "a"))))
(is (= ["should not be a"] (me/humanize (m/explain [:not= 'a] 'a))))))
(deftest not-humanize-test
(is (= ["should not be any"] (me/humanize (m/explain [:not any?] true))))
(is (= ["should not be some"] (me/humanize (m/explain [:not some?] true))))
(is (= ["should not be a number"] (me/humanize (m/explain [:not number?] 1))))
(is (= ["should not be an integer"] (me/humanize (m/explain [:not integer?] 1))))
(is (= ["should not be an int"] (me/humanize (m/explain [:not int?] 1))))
(is (= ["should not be a positive int"] (me/humanize (m/explain [:not pos-int?] 1))))
(is (= ["should not be a negative int"] (me/humanize (m/explain [:not neg-int?] -1))))
(is (= ["should not be a non-negative int"] (me/humanize (m/explain [:not nat-int?] 1))))
(is (= ["should not be positive"] (me/humanize (m/explain [:not pos?] 1))))
(is (= ["should not be negative"] (me/humanize (m/explain [:not neg?] -1))))
(is (= ["should not be a float"] (me/humanize (m/explain [:not float?] 1.23))))
(is (= ["should not be a double"] (me/humanize (m/explain [:not double?] 1.23))))
(is (= ["should not be a boolean"] (me/humanize (m/explain [:not boolean?] true))))
(is (= ["should not be a string"] (me/humanize (m/explain [:not string?] ""))))
(is (= ["should not be an ident"] (me/humanize (m/explain [:not ident?] 'a))))
(is (= ["should not be a simple ident"] (me/humanize (m/explain [:not simple-ident?] 'a))))
(is (= ["should not be a qualified ident"] (me/humanize (m/explain [:not qualified-ident?] ::a))))
(is (= ["should not be a keyword"] (me/humanize (m/explain [:not keyword?] :a))))
(is (= ["should not be a simple keyword"] (me/humanize (m/explain [:not simple-keyword?] :a))))
(is (= ["should not be a qualified keyword"] (me/humanize (m/explain [:not qualified-keyword?] ::a))))
(is (= ["should not be a symbol"] (me/humanize (m/explain [:not symbol?] 'a))))
(is (= ["should not be a simple symbol"] (me/humanize (m/explain [:not simple-symbol?] 'a))))
(is (= ["should not be a qualified symbol"] (me/humanize (m/explain [:not qualified-symbol?] `a))))
(is (= ["should not be a uuid"] (me/humanize (m/explain [:not uuid?] (random-uuid)))))
(is (= ["should not be a uri"] (me/humanize (m/explain [:not uri?] (#?(:clj java.net.URI.
:cljs Uri.
:default (throw (ex-info "Create URI" {})))
"http://asdf.com")))))
#?(:clj (is (= ["should not be a decimal"] (me/humanize (m/explain [:not decimal?] 1M)))))
(is (= ["should not be an inst"] (me/humanize (m/explain [:not inst?] #inst "2018-04-27T18:25:37Z"))))
(is (= ["should not be seqable"] (me/humanize (m/explain [:not seqable?] nil))))
(is (= ["should not be indexed"] (me/humanize (m/explain [:not indexed?] []))))
(is (= ["should not be a map"] (me/humanize (m/explain [:not map?] {}))))
(is (= ["should not be a vector"] (me/humanize (m/explain [:not vector?] []))))
(is (= ["should not be a list"] (me/humanize (m/explain [:not list?] (list)))))
(is (= ["should not be a seq"] (me/humanize (m/explain [:not seq?] (list)))))
(is (= ["should not be a char"] (me/humanize (m/explain [:not char?] \a))))
(is (= ["should not be a set"] (me/humanize (m/explain [:not set?] #{}))))
(is (= ["should not be nil"] (me/humanize (m/explain [:not nil?] nil))))
(is (= ["should not be false"] (me/humanize (m/explain [:not false?] false))))
(is (= ["should not be true"] (me/humanize (m/explain [:not true?] true))))
(is (= ["should not be zero"] (me/humanize (m/explain [:not zero?] 0))))
#?(:clj (is (= ["should not be a rational"] (me/humanize (m/explain [:not rational?] 1/2)))))
(is (= ["should not be a coll"] (me/humanize (m/explain [:not coll?] []))))
(is (= ["should not be empty"] (me/humanize (m/explain [:not empty?] []))))
(is (= ["should not be associative"] (me/humanize (m/explain [:not associative?] []))))
(is (= ["should not be sequential"] (me/humanize (m/explain [:not sequential?] []))))
#?(:clj (is (= ["should not be a ratio"] (me/humanize (m/explain [:not ratio?] 1/2)))))
#?(:clj (is (= ["should not be bytes"] (me/humanize (m/explain [:not bytes?] (byte-array 0))))))
(is (= ["should not match regex"] (me/humanize (m/explain [:not [:re #""]] ""))))
(is (= ["should not be a valid function"] (me/humanize (m/explain [:not [:=> :cat :any]] (fn [])))))
(is (= ["should not be an ifn"] (me/humanize (m/explain [:not ifn?] (fn [])))))
(is (= ["should not be a fn"] (me/humanize (m/explain [:not fn?] (fn [])))))
(is (= ["should not be 1"] (me/humanize (m/explain [:not [:enum 1]] 1))))
(is (= ["should not be either 1, 2 or 3"] (me/humanize (m/explain [:not [:enum 1 2 3]] 1))))
(is (= ["should not be any"] (me/humanize (m/explain [:not :any] 1))))
(is (= ["should not be nil"] (me/humanize (m/explain [:not :nil] nil))))
(is (= ["should not be a string"] (me/humanize (m/explain [:not :string] "a"))))
(is (= ["should not be at least 1 character"] (me/humanize (m/explain [:not [:string {:min 1}]] "a"))))
(is (= ["should not be at most 1 character"] (me/humanize (m/explain [:not [:string {:max 1}]] "a"))))
(is (= ["should not be 1 character"] (me/humanize (m/explain [:not [:string {:min 1 :max 1}]] "a"))))
(is (= ["should not be an integer"] (me/humanize (m/explain [:not :int] 1))))
(is (= ["should not be at least 1"] (me/humanize (m/explain [:not [:int {:min 1}]] 1))))
(is (= ["should not be at most 1"] (me/humanize (m/explain [:not [:int {:max 1}]] 1))))
(is (= ["should not be 1"] (me/humanize (m/explain [:not [:int {:min 1 :max 1}]] 1))))
(is (= ["should not be a double"] (me/humanize (m/explain [:not :double] 1.5))))
(is (= ["should not be at least 1.5"] (me/humanize (m/explain [:not [:double {:min 1.5}]] 1.5))))
(is (= ["should not be at most 1.5"] (me/humanize (m/explain [:not [:double {:max 1.5}]] 1.5))))
(is (= ["should not be 1.5"] (me/humanize (m/explain [:not [:double {:min 1.5 :max 1.5}]] 1.5))))
(is (= ["should not be a boolean"] (me/humanize (m/explain [:not :boolean] true))))
(is (= ["should not be a keyword"] (me/humanize (m/explain [:not :keyword] :a))))
(is (= ["should not be a symbol"] (me/humanize (m/explain [:not :symbol] 'a))))
(is (= ["should not be a qualified keyword"] (me/humanize (m/explain [:not :qualified-keyword] ::a))))
(is (= ["should not be a qualified symbol"] (me/humanize (m/explain [:not :qualified-symbol] `a))))
(is (= ["should not be a uuid"] (me/humanize (m/explain [:not :uuid] (random-uuid)))))
(is (= ["should be at most 1"] (me/humanize (m/explain [:not [:> 1]] 2))))
(is (= ["should be smaller than 1"] (me/humanize (m/explain [:not [:>= 1]] 2))))
(is (= ["should be at least 1"] (me/humanize (m/explain [:not [:< 1]] 0))))
(is (= ["should be larger than 1"] (me/humanize (m/explain [:not [:<= 1]] 0))))
(is (= ["should not be 1"] (me/humanize (m/explain [:not [:= 1]] 1))))
(is (= ["should be 1"] (me/humanize (m/explain [:not [:not= 1]] nil)))))
(deftest nested-not-humanize-test
(testing ":="
(is (= ["should be 1"] (me/humanize (m/explain [:= 1] nil))))
(is (= ["should not be 1"] (me/humanize (m/explain [:not [:= 1]] 1))))
(is (= ["should be 1"] (me/humanize (m/explain [:not [:not [:= 1]]] nil))))
(is (= ["should not be 1"] (me/humanize (m/explain [:not [:not [:not [:= 1]]]] 1))))
(is (= ["should be 1"] (me/humanize (m/explain [:not [:not [:not [:not [:= 1]]]]] nil)))))
(testing ":>"
(is (= ["should be larger than 1"] (me/humanize (m/explain [:> 1] 0))))
(is (= ["should be at most 1"] (me/humanize (m/explain [:not [:> 1]] 2))))
(is (= ["should be larger than 1"] (me/humanize (m/explain [:not [:not [:> 1]]] 0))))
(is (= ["should be at most 1"] (me/humanize (m/explain [:not [:not [:not [:> 1]]]] 2))))
(is (= ["should be larger than 1"] (me/humanize (m/explain [:not [:not [:not [:not [:> 1]]]]] 0)))))
(testing ":>="
(is (= ["should be at least 1"] (me/humanize (m/explain [:>= 1] 0))))
(is (= ["should be smaller than 1"] (me/humanize (m/explain [:not [:>= 1]] 2))))
(is (= ["should be at least 1"] (me/humanize (m/explain [:not [:not [:>= 1]]] 0))))
(is (= ["should be smaller than 1"] (me/humanize (m/explain [:not [:not [:not [:>= 1]]]] 2))))
(is (= ["should be at least 1"] (me/humanize (m/explain [:not [:not [:not [:not [:>= 1]]]]] 0)))))
(testing ":<"
(is (= ["should be smaller than 1"] (me/humanize (m/explain [:< 1] 2))))
(is (= ["should be at least 1"] (me/humanize (m/explain [:not [:< 1]] 0))))
(is (= ["should be smaller than 1"] (me/humanize (m/explain [:not [:not [:< 1]]] 2))))
(is (= ["should be at least 1"] (me/humanize (m/explain [:not [:not [:not [:< 1]]]] 0))))
(is (= ["should be smaller than 1"] (me/humanize (m/explain [:not [:not [:not [:not [:< 1]]]]] 2)))))
(testing ":<="
(is (= ["should be at most 1"] (me/humanize (m/explain [:<= 1] 2))))
(is (= ["should be larger than 1"] (me/humanize (m/explain [:not [:<= 1]] 0))))
(is (= ["should be at most 1"] (me/humanize (m/explain [:not [:not [:<= 1]]] 2))))
(is (= ["should be larger than 1"] (me/humanize (m/explain [:not [:not [:not [:<= 1]]]] 0))))
(is (= ["should be at most 1"] (me/humanize (m/explain [:not [:not [:not [:not [:<= 1]]]]] 2))))))
(deftest custom-negating-test
(is (= ["should be a multiple of 3"]
(me/humanize (m/explain [:fn {:error/message {:en "should be a multiple of 3"}} #(= 0 (mod % 3))] 2))))
(is (= ["should not be a multiple of 3"]
(me/humanize (m/explain [:not [:fn {:error/message {:en "should be a multiple of 3"}} #(= 0 (mod % 3))]] 3))))
(is (= ["should not be a multiple of 3 negated=false"]
(me/humanize (m/explain [:fn {:error/fn {:en (fn [{:keys [negated]} _] (str "should not be a multiple of 3 negated="
(boolean negated)))}}
#(not= 0 (mod % 3))] 0))))
(is (= ["should be a multiple of 3 negating=true"]
(me/humanize (m/explain [:not [:fn {:error/fn {:en (fn [{:keys [negated]} _] (str "should not be a multiple of 3 negating="
(boolean negated)))}}
#(not= 0 (mod % 3))]] 1))))
(testing ":negated disables implicit negation"
(is (= ["should not avoid being a multiple of 3"]
(me/humanize (m/explain [:not [:fn {:error/fn {:en (fn [{:keys [negated]} _]
(if negated
(negated "should not avoid being a multiple of 3")
"should not be a multiple of 3"))}}
#(not= 0 (mod % 3))]] 1))))))
+121
View File
@@ -0,0 +1,121 @@
(ns malli.experimental.always-test
(:refer-clojure :exclude [destructure])
(:require [clojure.test :refer [deftest testing is]]
[malli.experimental :as mx])
#?(:clj (:require [malli.dev])))
(mx/defn ^:malli/always addition :- [:int {:min 0}]
[x :- [:int {:min 0}], y :- :int]
(+ x y))
(mx/defn addition-2 :- [:int {:min 0}]
{:malli/always true}
[x :- [:int {:min 0}], y :- :int]
(+ x y))
(mx/defn addition-multiarity :- [:int {:min 0}]
{:malli/always true}
([x :- [:int {:min 0}], y :- :int]
(+ x y))
([x :- [:int {:min 2}]]
x))
(mx/defn ^:malli/always addition-varargs :- [:int {:min 0}]
[& xs :- [:cat [:int {:min 0}] [:* :int]]]
(apply + xs))
(mx/defn ^:malli/always destructure :- [:map [:b :int]]
{:more "metadata"}
[[val, {:keys [a, b] :as m1}] :- [:tuple :int [:map [:a :string]]],
{c :foo :as m2} :- [:map [:foo :int]]]
{:val val
:a a
:b b
:m1 m1
:c c
:m2 m2})
(defn always-assertions []
(doseq [[f description] [[addition ":malli/always meta on var"]
[addition-2 ":malli/always meta inside defn"]
[addition-multiarity "multiple arities"]
[addition-varargs "varargs"]]]
(testing description
(is (= 3 (f 1 2))
"valid input works")
(is (= :malli.core/invalid-input
(try (f -2 1)
(catch #?(:clj Exception :cljs js/Error) e
(:type (ex-data e)))))
"invalid input throws")
(is (= :malli.core/invalid-output
(try (f 2 -3)
(catch #?(:clj Exception :cljs js/Error) e
(:type (ex-data e)))))
"invalid output throws")))
(testing "other arity of multiple arity function"
(is (= 3 (addition-multiarity 3))
"valid input works")
(is (= :malli.core/invalid-input
(try (addition-multiarity 1)
(catch #?(:clj Exception :cljs js/Error) e
(:type (ex-data e)))))
"invalid input throws"))
(testing "destructuring"
(is (= {:val 1 :a "foo" :b 3
:m1 {:a "foo" :b 3}
:c 4
:m2 {:foo 4 :bar 5}}
(destructure [1 {:a "foo" :b 3}]
{:foo 4 :bar 5}))
"valid input works")
(is (= :malli.core/invalid-input
(try (destructure [1 {:a 2 :b 3}]
{:foo 4 :bar 5})
(catch #?(:clj Exception :cljs js/Error) e
(:type (ex-data e)))))
"invalid input throws")
(is (= :malli.core/invalid-input
(try (destructure [1 {:a "foo" :b 3}]
{:bar 5})
(catch #?(:clj Exception :cljs js/Error) e
(:type (ex-data e)))))
"invalid input throws")
(is (= :malli.core/invalid-output
(try (destructure [1 {:a "foo" :b true}]
{:foo 4 :bar 5})
(catch #?(:clj Exception :cljs js/Error) e
(:type (ex-data e)))))
"invalid output throws")))
(deftest always-test
(testing "without malli.dev"
(always-assertions))
#?(:clj
(do
(testing "with malli.dev/start!"
(malli.dev/start!)
(try
(always-assertions)
(finally
(malli.dev/stop!))))
(testing "after malli.dev/stop!"
(always-assertions)))))
(mx/defn destructure2 :- [:map [:b :int]]
{:more "metadata"}
[[val, {:keys [a, b] :as m1}] :- [:tuple :int [:map [:a :string]]],
{c :foo :as m2} :- [:map [:foo :int]]]
{:val val
:a a
:b b
:m1 m1
:c c
:m2 m2})
#?(:clj
(deftest always-metadata-test
(let [clean #(dissoc % :name :line :malli/always)]
(is (= (clean (meta #'destructure2))
(clean (meta #'destructure)))
":malli/always doesn't affect generated metadata"))))
+105
View File
@@ -0,0 +1,105 @@
(ns malli.experimental.describe-test
(:require [clojure.test :refer [deftest is testing]]
[malli.experimental.describe :as med]))
(deftest descriptor-test
(testing "vector"
(is (= "vector" (med/describe vector?)))
(is (= "vector of integer" (med/describe [:vector :int]))))
(testing "string"
(is (= "string with length >= 5" (med/describe [:string {:min 5}])))
(is (= "string with length <= 5" (med/describe [:string {:max 5}])))
(is (= "string with length between 3 and 5 inclusive" (med/describe [:string {:min 3 :max 5}]))))
(testing "function"
(is (= "function that takes input: [integer] and returns integer"
(med/describe [:=> [:cat int?] int?])))
(is (= "function that takes input: [integer] and returns integer"
(med/describe [:-> int? int?]))))
(testing "map"
(is (= "map" (med/describe map?)))
(is (= "map where {:x -> <integer>}"
(med/describe [:map [:x int?]])))
(is (= "map where {:x (optional) -> <integer>, :y -> <boolean>}"
(med/describe [:map [:x {:optional true} int?] [:y :boolean]])))
(is (= "map where {:x -> <integer>} with no other keys"
(med/describe [:map {:closed true} [:x int?]])))
(is (= "map where {:x (optional) -> <integer>, :y -> <boolean>} with no other keys"
(med/describe [:map {:closed true} [:x {:optional true} int?] [:y :boolean]])))
(is (= "map where {:j-code -> <keyword, and has length 4>}"
(med/describe [:map [:j-code [:and
:keyword
[:fn {:description "has length 4"} #(= 4 (count (name %)))]]]])))
(is (= "map (titled: dict) from <integer> to <string>"
(med/describe [:map-of {:title "dict"} :int :string]))))
(testing "compound schemas"
(is (= "vector of sequence of set of integer"
(med/describe [:vector [:sequential [:set :int]]]))))
(testing "multi"
(is (= "one of <:dog = map where {:x -> <integer>} | :cat = anything> dispatched by the type of animal"
(med/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"
(med/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>"
(med/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
{: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\">"
(med/describe [:schema
{:registry {"ConsCell" [:maybe [:tuple :int [:ref "ConsCell"]]]}}
"ConsCell"]))))
(testing "int"
(is (= "integer greater than or equal to 0"
(med/describe [:int {:min 0}])))
(is (= "integer less than or equal to 1"
(med/describe [:int {:max 1}])))
(is (= "integer between 0 and 1 inclusive"
(med/describe [:int {:min 0 :max 1}]))))
(testing "repeat"
(is (= "repeat <integer> at least 1 time"
(med/describe [:repeat {:min 1} int?])))
(is (= "repeat <integer> at most 7 times"
(med/describe [:repeat {:max 7} int?])))
(is (= "repeat <integer> at least 1 time, up to 7 times"
(med/describe [:repeat {:min 1 :max 7} int?])))))
+36
View File
@@ -0,0 +1,36 @@
(ns malli.experimental.lite-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.experimental.lite :as l]))
(deftest schema-test
(let [lschema (l/schema
{:int int?
:opt (l/optional {:a int?})
:maybe (l/maybe {:a int?})
:set (l/set {:a int?})
:vector (l/vector {:a int?})
:nested {:int int?
:map-of (l/map-of int? {:a int?})
:tuple (l/tuple int? {:a int?})
:and (l/and {:a int?} :map)
:or (l/or {:a int?} {:b int?})}})
mschema (m/schema
[:map
[:int int?]
[:opt {:optional true} [:map [:a int?]]]
[:maybe [:maybe [:map [:a int?]]]]
[:set [:set [:map [:a int?]]]]
[:vector [:vector [:map [:a int?]]]]
[:nested [:map
[:int int?]
[:map-of [:map-of int? [:map [:a int?]]]]
[:tuple [:tuple int? [:map [:a int?]]]]
[:and [:and [:map [:a int?]] :map]]
[:or [:or [:map [:a int?]] [:map [:b int?]]]]]]])]
(is (= (m/form lschema) (m/form mschema)))
(testing "with options"
(let [options {:registry (assoc (m/default-schemas) ::id :int)}]
(is (= (m/form (binding [l/*options* options] (l/schema {:id ::id, :nested {:id ::id}})))
(m/form [:map [:id ::id] [:nested [:map [:id ::id]]]] options)))))))
+31
View File
@@ -0,0 +1,31 @@
(ns malli.experimental.time.generator-test
(:require [malli.generator :as mg]
[malli.core :as m]
[malli.experimental.time-test :refer [r]]
[malli.experimental.time.generator]
[clojure.test :as t]
#?(:cljs [malli.experimental.time :refer [LocalDate LocalTime]]))
#?(:clj (:import (java.time LocalDate LocalTime))))
(defn exercise [schema]
(let [schema (m/schema schema {:registry r})
v (m/validator schema {:registry r})
g (mg/generator schema {:registry r})]
(every? v (mg/sample g {:size 1000 :registry r}))))
(t/deftest generator-test
(t/testing "simple schemas"
(t/is (exercise :time/duration))
(t/is (exercise :time/period))
(t/is (exercise :time/zone-id))
(t/is (exercise :time/zone-offset))
(t/is (exercise :time/instant))
(t/is (exercise :time/zoned-date-time))
(t/is (exercise :time/offset-date-time))
(t/is (exercise :time/offset-time))
(t/is (exercise :time/local-date))
(t/is (exercise :time/local-time))
(t/is (exercise :time/local-date-time)))
(t/testing "min max"
(t/is (exercise [:time/local-date {:min (. LocalDate parse "1980-01-02") :max (. LocalDate parse "1982-03-02")}]))
(t/is (exercise [:time/local-time {:min (. LocalTime parse "12:00:00") :max (. LocalTime parse "18:00:00")}]))))
@@ -0,0 +1,25 @@
(ns malli.experimental.time.json-schema-test
(:require [malli.experimental.time-test :refer [r]]
[malli.experimental.time.json-schema]
[malli.json-schema :as json]
[clojure.test :as t]))
(t/deftest time-formats
(t/is
(= {:type "object",
:properties {:date {:$ref "#/definitions/time.local-date"},
:time {:$ref "#/definitions/time.offset-time"},
:date-time {:$ref "#/definitions/time.offset-date-time"},
:duration {:$ref "#/definitions/time.duration"}},
:required [:date :time :date-time :duration],
:definitions {"time.local-date" {:type "string", :format "date"},
"time.offset-time" {:type "string", :format "time"},
"time.offset-date-time" {:type "string", :format "date-time"},
"time.duration" {:type "string", :format "duration"}}}
(json/transform
[:map
[:date :time/local-date]
[:time :time/offset-time]
[:date-time :time/offset-date-time]
[:duration :time/duration]]
{:registry r}))))
+85
View File
@@ -0,0 +1,85 @@
(ns malli.experimental.time.transform-test
(:require [malli.core :as m]
[malli.experimental.time-test :refer [r]]
[malli.experimental.time.transform :as time.transform]
#?(:cljs [malli.experimental.time
:refer [Duration Period LocalDate LocalDateTime LocalTime Instant OffsetTime ZonedDateTime OffsetDateTime ZoneId ZoneOffset
TemporalAccessor TemporalQuery DateTimeFormatter createTemporalQuery]])
[clojure.test :as t])
#?(:clj (:import [java.time Duration Period ZoneId])))
(defn validate
([schema v]
(validate schema v {:registry r}))
([schema v options]
(validate schema v time.transform/time-transformer options))
([schema v transformer options]
(m/validate schema (m/decode schema v options transformer) options)))
(t/deftest decode
(t/testing "Duration"
(t/is (validate :time/duration "PT0.01S"))
(t/is (not (validate :time/duration 10))))
(t/testing "Period"
(t/is (validate :time/period "P-1Y10D"))
(t/is (validate :time/period "P-1Y8M"))
(t/is (not (validate :time/period 10))))
(t/testing "zone id"
(t/is (validate :time/zone-id "UTC"))
(t/is (not (validate :time/zone-id "UTC'"))))
(t/testing "zone offset"
(t/is (validate :time/zone-offset "+15:00"))
(t/is (not (validate :time/zone-offset "UTC"))))
(t/testing "local date"
(t/is (validate :time/local-date "2020-01-01"))
(t/testing "Pattern"
(t/is (validate [:time/local-date {:pattern "yyyyMMdd"}] "20200101")))
(t/is (not (validate :time/local-date "202001-01"))))
(t/testing "local time"
(t/is (validate :time/local-time "12:00:00"))
(t/is (not (validate :time/local-time "$12:00:00"))))
(t/testing "local date time"
(t/is (validate :time/local-date-time "2020-01-01T12:00:00"))
(t/is (not (validate :time/local-date-time "2x020-01-01T12:00:00"))))
(t/testing "instant"
(t/is (validate :time/instant "2022-12-18T12:00:25.840823567Z"))
(t/is (not (validate :time/instant "2022-12-ABC18T12:00:25.840823567Z"))))
(t/testing "zoned date time"
(t/is (validate :time/zoned-date-time "2022-12-18T12:00:25.840823567Z[UTC]"))
(t/is (validate :time/zoned-date-time "2022-12-18T06:00:25.840823567-06:00[America/Chicago]"))
(t/is (not (validate :time/zoned-date-time "20%22-12-18T12:00:25.840823567Z[UTC]"))))
(t/testing "offset date time"
(t/is (validate :time/offset-date-time "2022-12-18T12:00:25.840823567Z"))
(t/is (validate :time/offset-date-time "2022-12-18T06:00:25.840823567-06:00"))
(t/is (not (validate :time/offset-date-time "2_022-12-18T12:00:25.840823567Z"))))
(t/testing "Aggregates"
(t/is (validate [:map [:date :time/local-date]] {:date "2020-01-01"}))))
(defn -decode [schema v]
(m/decode schema v {:registry r} time.transform/time-transformer))
(defn -encode [schema v]
(m/encode schema v {:registry r} time.transform/time-transformer))
(t/deftest encode
(t/testing "Encoding durations"
(t/is (= "PT24H" (-encode :time/duration (. Duration ofDays 1)))))
(t/testing "Encoding a period"
(t/is (= "P2M" (-encode :time/period (. Period ofMonths 2)))))
(t/testing "Encoding a zone id"
(t/is (= "EET" (-encode :time/zone-id (. ZoneId of "EET")))))
(t/testing "nil is nil"
(t/is (nil? (-encode [:maybe :time/duration] nil)))
(t/is (nil? (-encode [:maybe :time/period] nil)))
(t/is (nil? (-encode [:maybe :time/zone-id] nil))))
(t/testing "Round trip with patterns"
(t/is (= "20200101" (->> "20200101"
(-decode [:time/local-date {:pattern "yyyyMMdd"}])
(-encode [:time/local-date {:pattern "yyyyMMdd"}]))))
(t/is (= "2020_01_01" (->> "20200101"
(-decode [:time/local-date {:pattern "yyyyMMdd"}])
(-encode [:time/local-date {:pattern "yyyy_MM_dd"}]))))))
+144
View File
@@ -0,0 +1,144 @@
(ns ^:simple malli.experimental.time-test
(:require [malli.core :as m]
[malli.registry :as mr]
#?(:clj [malli.experimental.time :as time]
:cljs [malli.experimental.time :as time
:refer [Duration Period LocalDate LocalDateTime LocalTime Instant ZonedDateTime OffsetDateTime ZoneId OffsetTime]])
[clojure.test :as t])
#?(:clj (:import (java.time Duration Period LocalDate LocalDateTime LocalTime Instant ZonedDateTime OffsetDateTime ZoneId OffsetTime))))
(t/deftest compare-dates
(t/is (time/<= (. LocalDate parse "2020-01-01")
(. LocalDate parse "2020-01-01")))
(t/is (time/<= (. LocalDate parse "2020-01-01")
(. LocalDate parse "2020-01-02")))
(t/is (time/<= (. Duration ofMillis 10)
(. Duration ofMillis 11)))
(t/is (not (time/<= (. LocalDate parse "2020-01-02")
(. LocalDate parse "2020-01-01")))))
(def r
(mr/composite-registry
m/default-registry
(mr/registry (time/schemas))))
(t/deftest basic-types
(t/testing "Duration"
(t/is (m/validate :time/duration (. Duration ofMillis 10) {:registry r}))
(t/is (not (m/validate :time/duration 10 {:registry r}))))
(t/testing "Period"
(t/is (m/validate :time/period (. Period of 10 1 2) {:registry r}))
(t/is (not (m/validate :time/period 10 {:registry r}))))
(t/testing "zone id"
(t/is (m/validate :time/zone-id (. ZoneId of "UTC") {:registry r}))
(t/is (not (m/validate :time/zone-id "UTC" {:registry r}))))
(t/testing "local date"
(t/is (m/validate :time/local-date (. LocalDate parse "2020-01-01") {:registry r}))
(t/is (not (m/validate :time/local-date "2020-01-01" {:registry r}))))
(t/testing "local time"
(t/is (m/validate :time/local-time (. LocalTime parse "12:00:00") {:registry r}))
(t/is (not (m/validate :time/local-time "12:00:00" {:registry r}))))
(t/testing "offset time"
(t/is (m/validate :time/offset-time (. OffsetTime parse "12:00:00+00:00") {:registry r}))
(t/is (not (m/validate :time/offset-time "12:00:00" {:registry r}))))
(t/testing "local date time"
(t/is (m/validate :time/local-date-time (. LocalDateTime parse "2020-01-01T12:00:00") {:registry r}))
(t/is (not (m/validate :time/local-date-time "2020-01-01T12:00:00" {:registry r}))))
(t/testing "instant"
(t/is (m/validate :time/instant (. Instant parse "2022-12-18T12:00:25.840823567Z") {:registry r}))
(t/is (not (m/validate :time/instant "2022-12-18T12:00:25.840823567Z" {:registry r}))))
(t/testing "zoned date time"
(t/is (m/validate :time/zoned-date-time (. ZonedDateTime parse "2022-12-18T12:00:25.840823567Z[UTC]") {:registry r}))
(t/is (m/validate :time/zoned-date-time (. ZonedDateTime parse "2022-12-18T06:00:25.840823567-06:00[America/Chicago]") {:registry r}))
(t/is (not (m/validate :time/zoned-date-time "2022-12-18T12:00:25.840823567Z[UTC]" {:registry r}))))
(t/testing "offset date time"
(t/is (m/validate :time/offset-date-time (. OffsetDateTime parse "2022-12-18T12:00:25.840823567Z") {:registry r}))
(t/is (m/validate :time/offset-date-time (. OffsetDateTime parse "2022-12-18T06:00:25.840823567-06:00") {:registry r}))
(t/is (not (m/validate :time/offset-date-time "2022-12-18T12:00:25.840823567Z" {:registry r})))))
(t/deftest min-max
(t/testing "Duration"
(t/is (-> [:time/duration {:min (. Duration ofMillis 9) :max (. Duration ofMillis 10)}]
(m/validate (. Duration ofMillis 10) {:registry r})))
(t/is (-> [:time/duration {:min (. Duration ofMillis 9) :max (. Duration ofMillis 10)}]
(m/validate (. Duration ofMillis 12) {:registry r})
not)))
(t/testing "Period"
(t/is (-> [:time/period {:min (. Period ofYears 9) :max (. Period ofYears 10)}]
(m/validate (. Period ofYears 10) {:registry r})))
(t/is (-> [:time/period {:min (. Period ofMonths 9) :max (. Period ofMonths 10)}]
(m/validate (. Period ofMonths 12) {:registry r})
not))
(t/is (-> [:time/period {:min (. Period ofMonths 9) :max (. Period ofMonths 10)}]
(m/validate (. Period ofDays 12) {:registry r})
not))
(t/is (-> [:time/period {:min (. Period ofYears 9)}]
(m/validate (. Period ofYears 9) {:registry r})))
(t/is (-> [:time/period {:min (. Period ofYears 9)}]
(m/validate (. Period ofYears 10) {:registry r})))
(t/is (-> [:time/period {:min (. Period ofYears 9)}]
(m/validate (. Period ofYears 8) {:registry r})
not))
(t/is (-> [:time/period {:min (. Period of 0 10 2)}]
(m/validate (. Period of 1 9 3) {:registry r})))
(t/is (-> [:time/period {:max (. Period ofYears 9)}]
(m/validate (. Period ofYears 9) {:registry r})))
(t/is (-> [:time/period {:max (. Period ofYears 9)}]
(m/validate (. Period ofYears 8) {:registry r})))
(t/is (-> [:time/period {:max (. Period ofYears 9)}]
(m/validate (. Period ofDays 8) {:registry r})))
(t/is (-> [:time/period {:max (. Period ofYears 1)}]
(m/validate (. Period ofMonths 23) {:registry r})))
(t/is (-> [:time/period {:max (. Period ofYears 9)}]
(m/validate (. Period ofYears 10) {:registry r})
not))
(t/is (-> [:time/period {:max (. Period of 0 10 2)}]
(m/validate (. Period of 1 9 3) {:registry r})
not)))
(t/testing "local date"
(t/is (-> [:time/local-date {:min (. LocalDate parse "2020-01-01") :max (. LocalDate parse "2020-01-03")}]
(m/validate (. LocalDate parse "2020-01-01") {:registry r})))
(t/is (-> [:time/local-date {:min (. LocalDate parse "2020-01-01") :max (. LocalDate parse "2020-01-03")}]
(m/validate (. LocalDate parse "2020-01-04") {:registry r})
not)))
(t/testing "local time"
(t/is (-> [:time/local-time {:min (. LocalTime parse "12:00:00") :max (. LocalTime parse "13:00:00")}]
(m/validate (. LocalTime parse "12:00:00") {:registry r})))
(t/is (-> [:time/local-time {:min (. LocalTime parse "12:00:00") :max (. LocalTime parse "13:00:00")}]
(m/validate (. LocalTime parse "14:00:00") {:registry r})
not)))
(t/testing "local date time"
(t/is (m/validate [:time/local-date-time
{:min (. LocalDateTime parse "2020-01-01T11:30:00")
:max (. LocalDateTime parse "2020-01-01T12:30:00")}]
(. LocalDateTime parse "2020-01-01T12:00:00") {:registry r}))
(t/is (-> [:time/local-date-time
{:min (. LocalDateTime parse "2020-01-01T11:30:00")
:max (. LocalDateTime parse "2020-01-01T12:30:00")}]
(m/validate (. LocalDateTime parse "2020-01-01T12:40:00") {:registry r})
not)))
(t/testing "instant"
(t/is (-> [:time/instant
{:min (. Instant parse "2022-12-18T11:30:25.840823567Z")
:max (. Instant parse "2022-12-18T12:30:25.840823567Z")}]
(m/validate (. Instant parse "2022-12-18T12:00:25.840823567Z") {:registry r})))
(t/is (-> [:time/instant
{:min (. Instant parse "2022-12-18T11:30:25.840823567Z")
:max (. Instant parse "2022-12-18T12:30:25.840823567Z")}]
(m/validate (. Instant parse "2022-12-18T12:40:25.840823567Z") {:registry r})
not)))
(t/testing "zoned date time"
(t/is (-> [:time/zoned-date-time
{:min (. ZonedDateTime parse "2022-12-18T11:30:25.840823567Z[UTC]")
:max (. ZonedDateTime parse "2022-12-18T12:10:25.840823567Z[UTC]")}]
(m/validate (. ZonedDateTime parse "2022-12-18T12:00:25.840823567Z[UTC]") {:registry r})))
(t/is (-> [:time/zoned-date-time
{:min (. ZonedDateTime parse "2022-12-18T05:40:25.840823567-06:00[America/Chicago]")
:max (. ZonedDateTime parse "2022-12-18T12:10:25.840823567Z[UTC]")}]
(m/validate (. ZonedDateTime parse
"2022-12-18T06:00:25.840823567-06:00[America/Chicago]") {:registry r})))
(t/is (not (m/validate :time/zoned-date-time "2022-12-18T12:00:25.840823567Z[UTC]" {:registry r}))))
(t/testing "offset date time"
(t/is (m/validate :time/offset-date-time (. OffsetDateTime parse "2022-12-18T12:00:25.840823567Z") {:registry r}))
(t/is (m/validate :time/offset-date-time (. OffsetDateTime parse "2022-12-18T06:00:25.840823567-06:00") {:registry r}))
(t/is (not (m/validate :time/offset-date-time "2022-12-18T12:00:25.840823567Z" {:registry r})))))
+107
View File
@@ -0,0 +1,107 @@
(ns malli.experimental.validate-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.error :as me]
[malli.experimental.validate :as mev]))
(deftest test-validate
(testing "simple case"
(let [even-schema (m/schema [:validate (fn [x]
(when-not (even? x)
[{:in []
:type :not-even
:value x}]))]
{:registry (mev/schemas)})]
(is (m/validate even-schema 4))
(is (nil? (m/explain even-schema 4)))
(is (not (m/validate even-schema 3)))
(is (= [{:path []
:in []
:schema even-schema
:value 3
:type :not-even}]
(:errors (m/explain even-schema 3))))))
(testing "nested paths"
(let [both-even-schema (m/schema [:validate (fn [x]
(if-not (map? x)
[{:in []
:type :not-map
:value x}]
(seq
(keep identity
[(when-not (even? (:a x))
{:in [:a]
:type :not-even
:value (:a x)})
(when-not (even? (:b x))
{:in [:b]
:type :not-even
:value (:b x)})]))))]
{:registry (mev/schemas)})
schema (m/schema [:map [:value both-even-schema]])]
(is (m/validate schema {:value {:a 2 :b 4}}))
(is (nil? (m/explain schema {:value {:a 2 :b 4}})))
(is (not (m/validate schema {:value [2 4]})))
(is (= [{:path [:value]
:in [:value]
:schema both-even-schema
:value [2 4]
:type :not-map}]
(:errors (m/explain schema {:value [2 4]}))))
(is (not (m/validate schema {:value {:a 3 :b 4}})))
(is (= [{:path [:value]
:in [:value :a]
:schema both-even-schema
:value 3
:type :not-even}]
(:errors (m/explain schema {:value {:a 3 :b 4}}))))
(is (not (m/validate schema {:value {:a 2 :b 3}})))
(is (= [{:path [:value]
:in [:value :b]
:schema both-even-schema
:value 3
:type :not-even}]
(:errors (m/explain schema {:value {:a 2 :b 3}}))))
(testing "multiple errors"
(is (not (m/validate schema {:value {:a 3 :b 3}})))
(is (= [{:path [:value]
:in [:value :a]
:schema both-even-schema
:value 3
:type :not-even}
{:path [:value]
:in [:value :b]
:schema both-even-schema
:value 3
:type :not-even}]
(:errors (m/explain schema {:value {:a 3 :b 3}})))))))
(testing "humanize"
(let [two-sub-errors (m/schema [:validate (fn [x]
[{:in [:a]
:value (:a x)
:type :error-for-a}
{:in [:b]
:value (:b x)
:type :error-for-b}])]
{:registry (mev/schemas)})
schema (m/schema [:map [:value two-sub-errors]])
value {:value {:a 1 :b "x"}}
]
(is (not (m/validate schema value)))
(is (= [{:path [:value]
:in [:value :a]
:schema two-sub-errors
:value 1
:type :error-for-a}
{:path [:value]
:in [:value :b]
:schema two-sub-errors
:value "x"
:type :error-for-b}]
(:errors (m/explain schema value))))
(is (= {:value {:a ["unknown error"] :b ["unknown error"]}}
(me/humanize (m/explain schema value))))
(is (= {:value {:a ["a can not be!"] :b ["b can not be \"x\""]}}
(me/humanize (m/explain schema value)
{:errors {:error-for-a {:error/message {:en "a can not be!"}}
:error-for-b {:error/fn {:en (fn [{:keys [value]} _] (str "b can not be " (pr-str value)))}}}}))))))
+166
View File
@@ -0,0 +1,166 @@
(ns malli.experimental-test
(:require [clojure.test :refer [deftest is testing]]
[malli.dev]
[malli.experimental :as mx]
[malli.instrument :as mi]))
;; normal, no-args
(mx/defn f1 [] 1)
;; normal, args
(mx/defn f2 [x] x)
;; schematized, arg
(mx/defn f3 [x :- :int] x)
;; schematized, many args
(mx/defn f4 :- [:int {:min 0}]
"int int -> int functions"
[x :- [:int {:min 0}], y :- :int]
(+ x y))
(def AB [:map [:a [:int {:min 0}]] [:b :int]])
(def CD [:map [:c [:int {:min 0}]] [:d :int]])
;; schematized, nested keywords args
(mx/defn f5 :- [:cat :int :int :int :int AB CD]
"Nested Keyword argument"
[[& {:keys [a b] :as m1} :- AB]
& {:keys [c d] :as m2} :- CD]
[a b c d m1 m2])
;; multi-arity
(mx/defn f6 :- [:int {:min 0}]
"docstring"
{:some "meta"}
([x :- [:int {:min 0}]] (inc x))
([x :- [:int {:min 0}], y :- :int] (+ x y))
([x :- [:int {:min 0}], y :- :int & zs :- [:* :int]] (apply + x y zs))
{:more "meta"})
(mx/defn inner-outer-no-schema
[{{inner :inner} :outer}]
inner)
(def expectations
[{:var #'f1
:calls [[nil 1]
[[1] ::throws]]
:instrumented [[nil 1]
[[1] ::throws]]}
{:var #'f2
:calls [[[1] 1]
[["kikka"] "kikka"]
[[] ::throws]
[[1 2] ::throws]]
:instrumented [[[1] 1]
[["kikka"] "kikka"]
[[] ::throws]
[[1 2] ::throws]]}
{:var #'f3
:meta {:arglists '([x])
:raw-arglists '[[x :- :int]]
:schema [:=> [:cat :int] :any]}
:calls [[[1] 1]
[["kikka"] "kikka"]
[[1 2] ::throws]]
:instrumented [[[1] 1]
[["kikka"] ::throws]
[[1 2] ::throws]]}
{:var #'f4
:meta {:doc "int int -> int functions"
:arglists '([x y])
:raw-arglists '([x :- [:int {:min 0}] y :- :int])
:schema [:=> [:cat [:int {:min 0}] :int] [:int {:min 0}]]}
:calls [[[1 2] 3]
[[-2 1] -1]
[[-1 -1] -2]
[[1 "2"] ::throws]]
:instrumented [[[1 2] 3]
[[-2 1] ::throws] ;; input
[[2 -3] ::throws] ;; ret
[[1 "2"] ::throws]]}
{:var #'f5
:meta {:arglists '([[& {:keys [a b], :as m1}] & {:keys [c d], :as m2}])
:raw-arglists '([[& {:keys [a b] :as m1} :- AB]
& {:keys [c d] :as m2} :- CD])
:schema [:=>
[:cat [:maybe [:cat AB]] CD]
[:cat :int :int :int :int AB CD]]}
:calls [[[[{:a 1, :b 2}] {:c 3, :d 4}]
[1 2 3 4 {:a 1, :b 2} {:c 3, :d 4}]]
[[[{:a -1, :b 2}] {:c 3, :d 4}]
[-1 2 3 4 {:a -1, :b 2} {:c 3, :d 4}]]]
:instrumented [[[[{:a 1, :b 2}] {:c 3, :d 4}]
[1 2 3 4 {:a 1, :b 2} {:c 3, :d 4}]]
[[[{:a -1, :b 2}] {:c 3, :d 4}]
::throws]]}
{:var #'f6
:meta {:arglists '([x] [x y] [x y & zs])
:raw-arglists '([x :- [:int {:min 0}]]
[x :- [:int {:min 0}] y :- :int]
[x :- [:int {:min 0}] y :- :int & zs :- [:* :int]])
:schema [:function
[:=> [:cat [:int {:min 0}]] [:int {:min 0}]]
[:=> [:cat [:int {:min 0}] :int] [:int {:min 0}]]
[:=> [:cat [:int {:min 0}] :int [:* :int]] [:int {:min 0}]]]}
:calls [[[1] 2]
[[-1] 0]
[[1 2] 3]
[[1 -2] -1]
[[-1 2] 1]
[[1 2 3 4] 10]
[[-1 2 3 4] 8]]
:instrumented [[[1] 2]
[[-1] ::throws]
[[1 2] 3]
[[1 -2] ::throws]
[[-1 2] ::throws]
[[1 2 3 4] 10]
[[-1 2 3 4] ::throws]]}
{:var #'inner-outer-no-schema
:calls [[[(list :outer [:not-inner])] nil]
[[{:outer {:inner "here"}}] "here"]
[[{:outer {:not-inner 'foo}}] nil]]
:instrumented [[[(list :outer [:not-inner])] ::throws]
[[{:outer {:inner "here"}}] "here"]
[[{:outer {:not-inner 'foo}}] nil]]}])
(defn -strument! [mode v]
(with-out-str
(mi/-strument!
{:mode mode
:filters [(mi/-filter-var #(= % v))]})))
(deftest defn-test
(require 'malli.experimental-test :reload)
(doseq [{:keys [var calls instrumented] :as e} expectations]
(testing "plain calls"
(doseq [[arg ret] calls]
(testing (pr-str (list* 'apply (-> var symbol name symbol) (vec arg)))
(if (= ::throws ret)
(is (thrown? Exception (apply var arg)))
(let [actual (try (apply var arg)
(catch Throwable ex
(println "Unexpected failure in plain call" e [arg ret])
(throw ex)))]
(is (= ret actual)))))))
(when-let [m (:meta e)]
(testing "meta"
(doseq [[k v] m]
(is (= v (k (meta var)))
(str k)))))
(when instrumented
(testing "instrumented calls"
(-strument! :instrument var)
(try
(doseq [[arg ret] instrumented]
(testing (pr-str (list* 'apply (-> var symbol name symbol) (vec arg)))
(if (= ::throws ret)
(is (thrown? Exception (apply var arg)))
(is (= ret (apply var arg))))))
(finally
(-strument! :unstrument var)))))))
+75
View File
@@ -0,0 +1,75 @@
(ns malli.generator-ast
"For inspecting a malli's generator as data. See `generator-ast`"
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.walk :as walk]
[clojure.test.check.generators :as tcgen]
[malli.generator :as mg]))
(let [s (-> (slurp (io/resource "malli/generator.cljc"))
;; change the namespace
(str/replace-first "(ns malli.generator" "(ns malli.generator-ast")
;; change the `gen` alias to the AST version
(str/replace-first "clojure.test.check.generators" "malli.generator-debug")
(str/replace #"::(\S[^\/]+?)" "::mg/$1"))]
;; eval ns form first so keywords can be resolved in the right namespace
(eval (read-string {:read-cond :allow :features #{:clj}} s))
(eval (read-string {:read-cond :allow :features #{:clj}} (str "(do " s ")"))))
(defn generator-ast
"Return a malli schema's generator as an AST."
([?schema]
(generator-ast ?schema nil))
([?schema options]
(walk/postwalk
(fn [g]
(if (mg/-unreachable-gen? g)
{:op :unreachable}
(or (-> g meta ::mg/generator-ast)
g)))
(generator ?schema (assoc options ::mg/generator-ast true)))))
(defn- qualify-in-ns [q]
{:pre [(qualified-symbol? q)]}
(or (when-some [v (get (ns-map *ns*) (symbol (name q)))]
(when (var? v)
(when (= q (symbol v))
(let [uq (symbol (name q))]
;; prevent recursive-gen bindings from shadowing globals
(when (not (re-matches #"recur\d+" (name uq)))
uq)))))
(when-some [nsym (some (fn [[asym ns]]
(when (= (symbol (namespace q))
(ns-name ns))
asym))
(ns-aliases *ns*))]
(symbol (name nsym) (name q)))
q))
(defmulti -generator-code (fn [ast _options] (:op ast)))
(defmethod -generator-code :any [_ _] (qualify-in-ns `tcgen/any))
(defmethod -generator-code :one-of [{:keys [generators]} options]
(list (qualify-in-ns `tcgen/one-of) (mapv #(-generator-code % options) generators)))
(defmethod -generator-code :return [{:keys [value]} _]
(list (qualify-in-ns `tcgen/return)
(cond->> value
(not ((some-fn string? keyword? nil? boolean? number?) value))
(list 'quote))))
(defmethod -generator-code :recursive-gen [{:keys [target rec-gen scalar-gen]} options]
(list (qualify-in-ns `tcgen/recursive-gen)
(list (qualify-in-ns `fn) [(symbol target)] (-generator-code rec-gen options))
(-generator-code scalar-gen options)))
;; TODO infer pretty name from :ref schema
(defmethod -generator-code :recur [{:keys [target]} options] (symbol target))
(defmethod -generator-code :tuple [{:keys [generators]} options]
(list* (qualify-in-ns `tcgen/tuple)
(mapv #(-generator-code % options) generators)))
(defn generator-code
"Return pretty code that can be evaluated in the current namespace
to create a generator for schema."
([?schema] (generator-code ?schema nil))
([?schema options]
(-> ?schema
(generator-ast options)
(-generator-code options))))
+319
View File
@@ -0,0 +1,319 @@
(ns malli.generator-ast-test
(:require [clojure.test :refer [deftest is]]
[clojure.test.check.generators :as tcgen]
[malli.generator-ast :as ast]))
(deftest generator-ast-test
(is (= '{:op :recursive-gen,
:target :recur0
:rec-gen
{:op :one-of,
:generators
[{:op :boolean}
{:op :tuple,
:generators [{:op :return, :value :not} {:op :boolean}]}
{:op :tuple,
:generators
[{:op :return, :value :and}
{:op :vector, :generator {:op :recur :target :recur0}}]}
{:op :tuple,
:generators
[{:op :return, :value :or}
{:op :vector, :generator {:op :recur :target :recur0}}]}]},
:scalar-gen
{:op :one-of,
:generators
[{:op :boolean}
{:op :tuple,
:generators [{:op :return, :value :not} {:op :boolean}]}
{:op :tuple,
:generators
[{:op :return, :value :and} {:op :return, :value ()}]}
{:op :tuple,
:generators
[{:op :return, :value :or} {:op :return, :value ()}]}]}}
(ast/generator-ast
[:schema
{:registry
{::formula
[:or
:boolean
[:tuple [:enum :not] :boolean]
[:tuple [:enum :and] [:* [:ref ::formula]]]
[:tuple [:enum :or] [:* [:ref ::formula]]]]}}
[:ref ::formula]])))
(is (= '{:op :recursive-gen,
:target :recur0
:rec-gen
{:op :one-of,
:generators
[{:op :boolean}
{:op :tuple,
:generators [{:op :return, :value :not} {:op :boolean}]}
{:op :tuple,
:generators
[{:op :return, :value :and}
{:op :vector-min
:generator {:op :recur :target :recur0}
:min 1}]}
{:op :tuple,
:generators
[{:op :return, :value :or}
{:op :vector-min
:generator {:op :recur :target :recur0}
:min 1}]}]},
:scalar-gen
{:op :one-of,
:generators
[{:op :boolean}
{:op :tuple,
:generators [{:op :return, :value :not} {:op :boolean}]}]}}
(ast/generator-ast
[:schema
{:registry
{::formula
[:or
:boolean
[:tuple [:enum :not] :boolean]
[:tuple [:enum :and] [:+ [:ref ::formula]]]
[:tuple [:enum :or] [:+ [:ref ::formula]]]]}}
[:ref ::formula]])))
(is (= '{:op :recursive-gen,
:target :recur0,
:rec-gen
{:op :tuple,
:generators
[{:op :return, :value "A"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :recursive-gen,
:target :recur1,
:rec-gen
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :recur, :target :recur0}
{:op :recur, :target :recur1}]}]}]}
{:op :recur, :target :recur0}]}]}]},
:scalar-gen
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :recur, :target :recur0}]}]}
{:op :recur, :target :recur0}]}]}]}}
{:op :recursive-gen,
:target :recur1,
:rec-gen
{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :recur, :target :recur0}
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :recur, :target :recur1}
{:op :recur, :target :recur0}]}]}]}]}]}]},
:scalar-gen
{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :recur, :target :recur0}
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :recur, :target :recur0}]}]}]}]}]}}]}]}]},
:scalar-gen
{:op :tuple,
:generators
[{:op :return, :value "A"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :one-of,
:generators
[{:op :recursive-gen,
:target :recur0,
:rec-gen
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :recur, :target :recur0}]}]}]}]},
:scalar-gen
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :return, :value nil}]}]}]}}
{:op :recursive-gen,
:target :recur0,
:rec-gen
{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :recur, :target :recur0}]}]}]}]},
:scalar-gen
{:op :tuple,
:generators
[{:op :return, :value "C"}
{:op :one-of,
:generators
[{:op :return, :value nil}
{:op :tuple,
:generators
[{:op :return, :value "B"}
{:op :return, :value nil}]}]}]}}]}]}]}}
(ast/generator-ast
[:schema
{:registry {::A [:tuple [:= "A"] [:maybe [:or [:ref ::B] [:ref ::C]]]]
::B [:tuple [:= "B"] [:maybe [:or [:ref ::C] [:ref ::A]]]]
::C [:tuple [:= "C"] [:maybe [:or [:ref ::A] [:ref ::B]]]]}}
[:ref ::A]]))))
(def this-ns *ns*)
(deftest generator-code-test
(is (= '(tcgen/recursive-gen
(fn [recur0]
(tcgen/tuple (tcgen/return "A")
(tcgen/one-of
[(tcgen/return nil)
(tcgen/one-of
[(tcgen/recursive-gen
(fn [recur1]
(tcgen/tuple (tcgen/return "B")
(tcgen/one-of
[(tcgen/return nil)
(tcgen/one-of
[(tcgen/tuple
(tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
(tcgen/one-of [recur0 recur1])]))
recur0])])))
(tcgen/tuple (tcgen/return "B")
(tcgen/one-of [(tcgen/return nil)
(tcgen/one-of
[(tcgen/tuple (tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
recur0]))
recur0])])))
(tcgen/recursive-gen
(fn [recur1]
(tcgen/tuple (tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
(tcgen/one-of
[recur0
(tcgen/tuple
(tcgen/return "B")
(tcgen/one-of [(tcgen/return nil)
(tcgen/one-of [recur1 recur0])]))])])))
(tcgen/tuple (tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
(tcgen/one-of
[recur0
(tcgen/tuple (tcgen/return "B")
(tcgen/one-of [(tcgen/return nil)
recur0]))])])))])])))
(tcgen/tuple (tcgen/return "A")
(tcgen/one-of [(tcgen/return nil)
(tcgen/one-of
[(tcgen/recursive-gen
(fn [recur0]
(tcgen/tuple (tcgen/return "B")
(tcgen/one-of [(tcgen/return nil)
(tcgen/tuple
(tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
recur0]))])))
(tcgen/tuple (tcgen/return "B")
(tcgen/one-of [(tcgen/return nil)
(tcgen/tuple (tcgen/return "C")
(tcgen/return nil))])))
(tcgen/recursive-gen
(fn [recur0]
(tcgen/tuple (tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
(tcgen/tuple (tcgen/return "B")
(tcgen/one-of [(tcgen/return nil)
recur0]))])))
(tcgen/tuple (tcgen/return "C")
(tcgen/one-of [(tcgen/return nil)
(tcgen/tuple (tcgen/return "B")
(tcgen/return nil))])))])])))
(binding [*ns* this-ns]
(ast/generator-code
[:schema
{:registry {::A [:tuple [:= "A"] [:maybe [:or [:ref ::B] [:ref ::C]]]]
::B [:tuple [:= "B"] [:maybe [:or [:ref ::C] [:ref ::A]]]]
::C [:tuple [:= "C"] [:maybe [:or [:ref ::A] [:ref ::B]]]]}}
[:ref ::A]])))))
(deftest maybe-ast-test
(is (ast/generator-ast [:maybe :boolean])))
+50
View File
@@ -0,0 +1,50 @@
(ns malli.generator-debug
"Drop-in replacement for clojure.test.check.generators that returns AST's
instead of generators."
(:refer-clojure :exclude [vector char keyword boolean not-empty symbol])
(:require [clojure.core :as cc]))
(defmacro such-that [& args] (let [args (vec args)] `{:op :such-that :args-form '~args :args ~args}))
(def any {:op :any})
(def any-printable {:op :any-printable})
(defn double* [& args] {:op :double* :args args})
(defmacro fmap [& args] (let [args (vec args)] `{:op :fmap :args-form '~args :args ~args}))
(defmacro vector
([generator] {:op :vector :generator generator})
([generator num-elements] {:op :vector :generator generator :num-elements num-elements})
([generator min-elements max-elements]
{:op :vector :generator generator :min-elements min-elements :max-elements max-elements}))
(defmacro vector-distinct [& args] (let [args (vec args)] `{:op :vector-distinct :args-form '~args :args ~args}))
(defmacro vector-distinct-by [& args] (let [args (vec args)] `{:op :vector-distinct-by :args-form '~args :args ~args}))
(def char {:op :char})
(def nat {:op :nat})
(def char-alphanumeric {:op :char-alphanumeric})
(def string-alphanumeric {:op :string-alphanumeric})
(defn sized [& args] {:op :sized :args args})
(defn return [value] {:op :return :value value})
(defn one-of [generators] {:op :one-of :generators generators})
(defn tuple [& generators] {:op :tuple :generators (vec generators)})
(def ^:private ^:dynamic *recursion-depth* 0)
(defn recursive-gen [rec scalar]
(let [target (cc/keyword (str "recur" *recursion-depth*))]
{:op :recursive-gen
:target target
:rec-gen (binding [*recursion-depth* (inc *recursion-depth*)]
(rec {:op :recur
:target target}))
:scalar-gen scalar}))
(def keyword {:op :keyword})
(def keyword-ns {:op :keyword-ns})
(def symbol {:op :symbol})
(def symbol-ns {:op :symbol-ns})
(def s-pos-int {:op :s-pos-int})
(def s-neg-int {:op :s-neg-int})
(defn elements [coll] {:op :elements :coll coll})
(defn large-integer* [& args] {:op :large-integer* :args args})
(def boolean {:op :boolean})
(def uuid {:op :uuid})
(defn not-empty [gen] {:op :not-empty :gen gen})
(defn generator? [& args] (assert nil "no stub for generator?"))
(defn call-gen [& args] (assert nil "no stub for call-gen"))
(defn make-size-range-seq [& args] (assert nil "no stub for make-size-range-seq"))
(defn lazy-random-states [& args] (assert nil "no stub for lazy-random-states"))
File diff suppressed because it is too large Load Diff
+216
View File
@@ -0,0 +1,216 @@
(ns malli.instrument.cljs-test
(:require [cljs.test :refer [deftest is testing]]
[malli.instrument.fn-schemas :as schemas :refer [VecOfInts sum-nums sum-nums2 str-join str-join2 str-join3 str-join4]]
[malli.instrument.fn-schemas2 :as schemas-2]
[malli.core :as m]
[malli.experimental :as mx]
[malli.instrument.cljs :as mi]))
(defn plus [x] (inc x))
(m/=> plus [:=> [:cat :int] [:int {:max 6}]])
(def small-int [:int {:max 6}])
(defn minus
"kukka"
{:malli/schema [:=> [:cat :int] [:int {:min 6}]]
:malli/scope #{:input :output}}
[x] (dec x))
(defn multi-arity-fn
{:malli/schema
[:function
[:=> [:cat] [:int]]
[:=> [:cat :int] [:int]]
[:=> [:cat :string :string] [schemas-2/string]]]}
([] 500)
([a] (inc a))
([a b] (str a b)))
(defn multi-arity-variadic-fn
{:malli/schema
[:function
[:=> [:cat] [:int]]
[:=> [:cat :int] [schemas-2/int-arg]]
[:=> [:cat :string :string] [:string]]
[:=> [:cat :string :string [:* :string]] [:string]]]}
([] 500)
([a] (inc a))
([a b] (str a b))
([a b c & more] (str a b c more)))
(defn variadic-fn1
{:malli/schema [:=> [:cat [:* :int]] [:int]]}
[& vs] (apply + vs))
(defn variadic-fn2
{:malli/schema [:=> [:cat :int [:* :int]] [:int]]}
[a & vs] (apply + a vs))
(defn minus-small-int
"kukka"
{:malli/schema [:=> [:cat :int] small-int]
:malli/scope #{:input :output}}
[x] (dec x))
(defn plus-small-int [x] (inc x))
(m/=> plus-small-int [:=> [:cat :int] small-int])
(def Over100 (m/-simple-schema {:type 'Over100, :pred #(and (int? %) (< 100 %))}))
(defn plus-over-100 [x] (inc x))
(m/=> plus-over-100 [:=> [:cat :int] Over100])
(mx/defn power :- [:int {:max 6}]
"inlined schema power"
[x :- :int] (* x x))
(mx/defn str-join-mx :- int?
[args :- VecOfInts]
(apply str args))
(deftest instrument!-test
(testing "with instrumentation"
(mi/instrument! {:filters [(mi/-filter-ns 'malli.instrument.cljs-test 'malli.instrument.fn-schemas)]})
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (plus "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (plus 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (plus-small-int "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (plus-small-int 8)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (plus-over-100 "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (plus-over-100 8)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (power "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (power 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join-mx ["2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (str-join-mx [6])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/str-join-mx2 ["2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/str-join-mx2 [6])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-ret-refer "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-ret-refer 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-ret-ns "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-ret-ns 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-arg-refer "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-arg-refer 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-arg-ns "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-arg-ns 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-full "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-full 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-int? "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-int? 6))))
(testing "without instrumentation"
(mi/unstrument! {:filters [(mi/-filter-ns 'malli.instrument.cljs-test 'malli.instrument.fn-schemas)]})
(is (= "21" (plus "2")))
(is (= 7 (plus 6)))
(is (= (plus-small-int 8) 9))
(is (= (plus-over-100 8) 9))
(is (= 4 (power "2")))
(is (= 36 (power 6)))
(is (= "2" (str-join-mx ["2"])))
(is (= "6" (str-join-mx [6])))
(is (= "2" (schemas/str-join-mx2 ["2"])))
(is (= "6" (schemas/str-join-mx2 [6])))
(is (= 4 (schemas/power-ret-refer "2")))
(is (= 36 (schemas/power-ret-refer 6)))
(is (= 4 (schemas/power-ret-ns "2")))
(is (= 36 (schemas/power-ret-ns 6)))
(is (= 4 (schemas/power-arg-refer "2")))
(is (= 36 (schemas/power-arg-refer 6)))
(is (= 4 (schemas/power-arg-ns "2")))
(is (= 36 (schemas/power-arg-ns 6)))
(is (= 4 (schemas/power-full "2")))
(is (= 36 (schemas/power-full 6)))))
(mi/collect! {:ns ['malli.instrument.cljs-test 'malli.instrument.fn-schemas]})
(deftest collect!-test
(testing "with instrumentation"
(mi/instrument! {:filters [(mi/-filter-ns 'malli.instrument.cljs-test 'malli.instrument.fn-schemas)]})
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join2 [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join3 [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join4 [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (sum-nums "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (sum-nums2 "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (minus "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (minus 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (minus-small-int "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (minus-small-int 10)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (variadic-fn1 1 "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (variadic-fn2 1 "2")))
(is (= 3 (variadic-fn1 1 2)))
(is (= 3 (variadic-fn2 1 2)))
(is (= 500 (multi-arity-fn)))
(is (= 2 (multi-arity-fn 1)))
(is (= "ab" (multi-arity-fn "a" "b")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-fn "a")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-fn 1 2)))
(is (= 500 (multi-arity-variadic-fn)))
(is (= 2 (multi-arity-variadic-fn 1)))
(is (= "ab" (multi-arity-variadic-fn "a" "b")))
(is (= "abc(\"d\")" (multi-arity-variadic-fn "a" "b" "c" "d")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-variadic-fn "a")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-variadic-fn 1 2)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-variadic-fn 1 2 :c))))
(testing "without instrumentation"
(mi/unstrument! {:filters [(mi/-filter-ns 'malli.instrument.cljs-test 'malli.instrument.fn-schemas)]})
(is (= 1 (minus "2")))
(is (= 5 (minus 6)))
(is (= (sum-nums [2 "3" 4 5]) "2345"))
(is (= (sum-nums2 [2 "3" 4 5]) "2345"))
(is (= (str-join [1 "2"]) "12"))
(is (= (str-join2 [1 "2"]) "12"))
(is (= (str-join3 [1 "2"]) "12"))
(is (= (str-join4 [1 "2"]) "12"))
(is (= 1 (minus-small-int "2")))
(is (= 9 (minus-small-int 10)))))
(deftest check-test
(let [results (mi/check)]
(is (map? results))))
(deftest instrument-external-test
(testing "Without instrumentation"
(is (thrown?
js/Error
#_:clj-kondo/ignore
(select-keys {:a 1} :a))))
(testing "With instrumentation"
(m/=> clojure.core/select-keys [:=> [:cat map? sequential?] map?])
(with-out-str (mi/instrument! {:filters [(mi/-filter-ns 'clojure.core)]}))
(is (thrown-with-msg?
js/Error
#":malli.core/invalid-input"
#_:clj-kondo/ignore
(select-keys {:a 1} :a)))
(is (= {:a 1} (select-keys {:a 1} [:a])))
(with-out-str (mi/unstrument! {:filters [(mi/-filter-ns 'clojure.core)]}))))
+59
View File
@@ -0,0 +1,59 @@
(ns malli.instrument.fn-schemas
(:require [malli.experimental :as mx]
[malli.instrument.fn-schemas2 :as schemas :refer [small-int int-arg VecOfStrings]]))
(def VecOfInts [:vector :int])
(defn sum-nums
{:malli/schema [:=> [:cat VecOfInts] :int]}
[args]
(apply + args))
(defn sum-nums2
{:malli/schema [:=> [:cat VecOfInts] float?]}
[args]
(apply + args))
(defn str-join
{:malli/schema [:=> [:cat VecOfStrings] schemas/string]}
[args]
(apply str args))
(def str-join-schema [:=> [:cat VecOfStrings] schemas/string])
(defn str-join2
{:malli/schema [:-> VecOfStrings schemas/string]}
[args]
(apply str args))
(defn str-join3
{:malli/schema str-join-schema}
[args]
(apply str args))
(defn str-join4
{:malli/schema [:=> [:cat malli.instrument.fn-schemas2/VecOfStrings] schemas/string]}
[args]
(apply str args))
(mx/defn str-join-mx2 :- int?
[args :- VecOfInts]
(apply str args))
(mx/defn power-ret-refer :- small-int
[x :- :int] (* x x))
(mx/defn power-ret-ns :- schemas/small-int
[x :- :int] (* x x))
(mx/defn power-arg-refer :- [:int {:max 6}]
[x :- int-arg] (* x x))
(mx/defn power-arg-ns :- [:int {:max 6}]
[x :- schemas/int-arg] (* x x))
(mx/defn power-full :- malli.instrument.fn-schemas2/small-int
[x :- malli.instrument.fn-schemas2/int-arg] (* x x))
(mx/defn power-int? :- small-int
[x :- int?] (* x x))
+10
View File
@@ -0,0 +1,10 @@
(ns malli.instrument.fn-schemas2)
(def VecOfStrings [:vector :string])
(def string :string)
(def small-int
[:int {:max 6}])
(def int-arg :int)
+161
View File
@@ -0,0 +1,161 @@
(ns malli.instrument-test
(:require [clojure.string :as str]
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.instrument :as mi]))
(defn plus [x] (inc x))
(m/=> plus [:=> [:cat :int] [:int {:max 6}]])
(defn ->plus [] plus)
(defn primitive-DO [^double val] val)
(m/=> primitive-DO [:=> [:cat :double] :double])
(defn opts [] {:filters [(fn [& args]
(and (apply (mi/-filter-ns 'malli.instrument-test) args)
(apply (mi/-filter-var #(not= #'primitive-DO %)) args)))]})
(defn unstrument! [] (with-out-str (mi/unstrument! (opts))))
(defn instrument! [] (with-out-str (mi/instrument! (opts))))
(deftest instrument!-test
(testing "without instrumentation"
(unstrument!)
(is (thrown?
ClassCastException
((->plus) "2")))
(is (= 7 ((->plus) 6))))
(testing "with instrumentation"
(instrument!)
(is (thrown-with-msg?
Exception
#":malli.core/invalid-input"
((->plus) "2")))
(is (thrown-with-msg?
Exception
#":malli.core/invalid-output"
((->plus) 6)))))
(defmacro if-bb [then & [else]]
(if (System/getProperty "babashka.version")
then
else))
(if-bb
(deftest primitive-functions-can-be-instrumented
(is (= 42 (primitive-DO 42)))
(is (= "42" (primitive-DO "42")))
(is (mi/instrument! {:filters [(mi/-filter-var #(= #'primitive-DO %))]}))
(is (thrown-with-msg?
Exception
#":malli.core/invalid-input"
(primitive-DO "42")))
(is (mi/unstrument! {:filters [(mi/-filter-var #(= #'primitive-DO %))]}))
(is (= 42 (primitive-DO 42)))
(is (= "42" (primitive-DO "42"))))
(deftest primitive-functions-cannot-be-instrumented
(is (= 42.0 (primitive-DO 42)))
(is (thrown? ClassCastException (primitive-DO "42")))
(is (str/includes?
(with-out-str (mi/instrument! {:filters [(mi/-filter-var #(= #'primitive-DO %))]}))
"WARNING: Not instrumenting primitive fn #'malli.instrument-test/primitive-DO"))
(is (= 42.0 (primitive-DO 42)))
(is (thrown? ClassCastException (primitive-DO "42")))))
(defn minus
"kukka"
{:malli/schema [:=> [:cat :int] [:int {:min 6}]]
:malli/scope #{:input :output}}
[x] (dec x))
(defn ->minus [] minus)
(mi/collect!)
(deftest collect!-test
(testing "without instrumentation"
(unstrument!)
(is (thrown?
ClassCastException
((->minus) "2")))
(is (= 5 ((->minus) 6))))
(testing "with instrumentation"
(instrument!)
(is (thrown-with-msg?
Exception
#":malli.core/invalid-input"
((->minus) "2")))
(is (thrown-with-msg?
Exception
#":malli.core/invalid-output"
((->minus) 6)))))
(defn f1
"accumulated schema from arities"
(^{:malli/schema [:=> [:cat :int] :int]} [x] (inc x))
(^{:malli/schema [:=> [:cat :int :int] :int]} [x y] (+ x y)))
(defn f2
"top-level schema wins"
(^{:malli/schema [:=> [:cat :any] :any]} [x] (inc x))
(^{:malli/schema [:=> [:cat :any :any] :any]} [x y] (+ x y))
{:malli/schema [:function
[:=> [:cat :int] :int]
[:=> [:cat :int :int] :int]]})
(defn f3
"invalid schema as not arities have it"
([x] (inc x))
(^{:malli/schema [:=> [:cat :int :int] :int]} [x y] (+ x y)))
(deftest -schema-test
(is (= [:function
[:=> [:cat :int] :int]
[:=> [:cat :int :int] :int]]
(mi/-schema #'f1)))
(is (= [:function
[:=> [:cat :int] :int]
[:=> [:cat :int :int] :int]]
(mi/-schema #'f2)))
(is (= nil (mi/-schema #'f3))))
(deftest check-test
(testing "all registered function schemas in this namespace"
(let [results (mi/check {:filters [(mi/-filter-ns 'malli.instrument-test)]})]
(is (map? results)))))
(deftest instrument-external-test
(testing "Without instrumentation"
(is (thrown?
java.lang.IllegalArgumentException
#_:clj-kondo/ignore
(select-keys {:a 1} :a))))
(testing "With instrumentation"
(m/=> clojure.core/select-keys [:=> [:cat map? sequential?] map?])
(with-out-str (mi/instrument! {:filters [(mi/-filter-ns 'clojure.core)]}))
(is (thrown-with-msg?
Exception
#":malli.core/invalid-input"
#_:clj-kondo/ignore
(select-keys {:a 1} :a)))
(is (= {:a 1} (select-keys {:a 1} [:a])))
(with-out-str (mi/unstrument! {:filters [(mi/-filter-ns 'clojure.core)]}))))
(defn reinstrumented [] 1)
(deftest reinstrument-test
(m/=> reinstrumented [:-> [:= 2]])
(instrument!)
(is (thrown-with-msg?
Exception
#":malli\.core/invalid-output"
(reinstrumented)))
(m/=> reinstrumented [:-> [:= 1]])
(instrument!)
(is (= 1 (reinstrumented))))
+218
View File
@@ -0,0 +1,218 @@
(ns malli.instrument-test
(:require [cljs.test :refer [deftest is testing]]
[malli.instrument.fn-schemas :as schemas :refer [VecOfInts sum-nums sum-nums2 str-join str-join2 str-join3 str-join4]]
[malli.instrument.fn-schemas2 :as schemas-2]
[malli.core :as m]
[malli.experimental :as mx]
[malli.instrument :as mi]))
(defn plus [x] (inc x))
(m/=> plus [:=> [:cat :int] [:int {:max 6}]])
(def small-int [:int {:max 6}])
(defn minus
"kukka"
{:malli/schema [:=> [:cat :int] [:int {:min 6}]]
:malli/scope #{:input :output}}
[x] (dec x))
(defn multi-arity-fn
{:malli/schema
[:function
[:=> [:cat] [:int]]
[:=> [:cat :int] [:int]]
[:=> [:cat :string :string] [schemas-2/string]]]}
([] 500)
([a] (inc a))
([a b] (str a b)))
(defn multi-arity-variadic-fn
{:malli/schema
[:function
[:=> [:cat] [:int]]
[:=> [:cat :int] [schemas-2/int-arg]]
[:=> [:cat :string :string] [:string]]
[:=> [:cat :string :string [:* :string]] [:string]]]}
([] 500)
([a] (inc a))
([a b] (str a b))
([a b c & more] (str a b c more)))
(defn variadic-fn1
{:malli/schema [:=> [:cat [:* :int]] [:int]]}
[& vs] (apply + vs))
(defn variadic-fn2
{:malli/schema [:=> [:cat :int [:* :int]] [:int]]}
[a & vs] (apply + a vs))
(defn minus-small-int
"kukka"
{:malli/schema [:=> [:cat :int] small-int]
:malli/scope #{:input :output}}
[x] (dec x))
(defn plus-small-int [x] (inc x))
(m/=> plus-small-int [:=> [:cat :int] small-int])
(def Over100 (m/-simple-schema {:type 'Over100, :pred #(and (int? %) (< 100 %))}))
(defn plus-over-100 [x] (inc x))
(m/=> plus-over-100 [:=> [:cat :int] Over100])
(mx/defn power :- [:int {:max 6}]
"inlined schema power"
[x :- :int] (* x x))
(mx/defn str-join-mx :- int?
[args :- VecOfInts]
(apply str args))
(deftest ^:simple instrument!-test
(testing "with instrumentation"
(mi/instrument! {:filters [(mi/-filter-ns 'malli.instrument-test 'malli.instrument.fn-schemas)]})
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (plus "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (plus 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (plus-small-int "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (plus-small-int 8)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (plus-over-100 "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (plus-over-100 8)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (power "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (power 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join-mx ["2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (str-join-mx [6])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/str-join-mx2 ["2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/str-join-mx2 [6])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-ret-refer "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-ret-refer 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-ret-ns "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-ret-ns 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-arg-refer "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-arg-refer 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-arg-ns "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-arg-ns 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-full "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-full 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (schemas/power-int? "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (schemas/power-int? 6))))
(testing "without instrumentation"
(mi/unstrument! {:filters [(mi/-filter-ns 'malli.instrument-test 'malli.instrument.fn-schemas)]})
(is (= "21" (plus "2")))
(is (= 7 (plus 6)))
(is (= (plus-small-int 8) 9))
(is (= (plus-over-100 8) 9))
(is (= 4 (power "2")))
(is (= 36 (power 6)))
(is (= "2" (str-join-mx ["2"])))
(is (= "6" (str-join-mx [6])))
(is (= "2" (schemas/str-join-mx2 ["2"])))
(is (= "6" (schemas/str-join-mx2 [6])))
(is (= 4 (schemas/power-ret-refer "2")))
(is (= 36 (schemas/power-ret-refer 6)))
(is (= 4 (schemas/power-ret-ns "2")))
(is (= 36 (schemas/power-ret-ns 6)))
(is (= 4 (schemas/power-arg-refer "2")))
(is (= 36 (schemas/power-arg-refer 6)))
(is (= 4 (schemas/power-arg-ns "2")))
(is (= 36 (schemas/power-arg-ns 6)))
(is (= 4 (schemas/power-full "2")))
(is (= 36 (schemas/power-full 6)))))
(deftest ^:simple collect!-test
(mi/collect! {:ns ['malli.instrument-test 'malli.instrument.fn-schemas]})
(testing "with instrumentation"
(mi/instrument! {:filters [(mi/-filter-ns 'malli.instrument-test 'malli.instrument.fn-schemas)]})
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join2 [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join3 [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (str-join4 [1 "2"])))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (sum-nums "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (sum-nums2 "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (minus "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (minus 6)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (minus-small-int "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-output" (minus-small-int 10)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (variadic-fn1 1 "2")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (variadic-fn2 1 "2")))
(is (= 3 (variadic-fn1 1 2)))
(is (= 3 (variadic-fn2 1 2)))
(is (= 500 (multi-arity-fn)))
(is (= 2 (multi-arity-fn 1)))
(is (= "ab" (multi-arity-fn "a" "b")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-fn "a")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-fn 1 2)))
(is (= 500 (multi-arity-variadic-fn)))
(is (= 2 (multi-arity-variadic-fn 1)))
(is (= "ab" (multi-arity-variadic-fn "a" "b")))
(is (= "abc(\"d\")" (multi-arity-variadic-fn "a" "b" "c" "d")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-variadic-fn "a")))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-variadic-fn 1 2)))
(is (thrown-with-msg? js/Error #":malli.core/invalid-input" (multi-arity-variadic-fn 1 2 :c))))
(testing "without instrumentation"
(mi/unstrument! {:filters [(mi/-filter-ns 'malli.instrument-test 'malli.instrument.fn-schemas)]})
(is (= 1 (minus "2")))
(is (= 5 (minus 6)))
(is (= (sum-nums [2 "3" 4 5]) "2345"))
(is (= (sum-nums2 [2 "3" 4 5]) "2345"))
(is (= (str-join [1 "2"]) "12"))
(is (= (str-join2 [1 "2"]) "12"))
(is (= (str-join3 [1 "2"]) "12"))
(is (= (str-join4 [1 "2"]) "12"))
(is (= 1 (minus-small-int "2")))
(is (= 9 (minus-small-int 10)))))
(deftest ^:simple check-test
(testing "all registered function schemas in this namespace"
(let [results (mi/check {:filters [(mi/-filter-ns 'malli.instrument-test)]})]
(is (map? results)))))
(deftest ^:simple instrument-external-test
(testing "Without instrumentation"
(is (thrown?
js/Error
#_:clj-kondo/ignore
(select-keys {:a 1} :a))))
(testing "With instrumentation"
(m/=> cljs.core/select-keys [:=> [:cat map? sequential?] map?])
(with-out-str (mi/instrument! {:filters [(mi/-filter-ns 'cljs.core)]}))
(is (thrown-with-msg?
js/Error
#":malli.core/invalid-input"
#_:clj-kondo/ignore
(select-keys {:a 1} :a)))
(is (= {:a 1} (select-keys {:a 1} [:a])))
(with-out-str (mi/unstrument! {:filters [(mi/-filter-ns 'cljs.core)]}))))
+389
View File
@@ -0,0 +1,389 @@
(ns malli.json-schema-test
(:require [clojure.test.check.generators :as gen]
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.core-test]
[malli.json-schema :as json-schema]
[malli.util :as mu]))
(def expectations
[;; predicates
[pos-int? {:type "integer", :minimum 1}]
[pos? {:type "number" :exclusiveMinimum 0}]
[float? {:type "number"}]
;; comparators
[[:> 6] {:type "number", :exclusiveMinimum 6}]
[[:>= 6] {:type "number", :minimum 6}]
[[:< 6] {:type "number", :exclusiveMaximum 6}]
[[:<= 6] {:type "number", :maximum 6}]
[[:= "x"] {:const "x"}]
;; base
[[:not string?] {:not {:type "string"}}]
[[:and int? pos-int?] {:allOf [{:type "integer"}
{:type "integer", :minimum 1}]}]
[[:or int? string?] {:anyOf [{:type "integer"} {:type "string"}]}]
[[:orn [:i int?] [:s string?]] {:anyOf [{:type "integer"} {:type "string"}]}]
[[:map
[:a string?]
[:b {:optional true} string?]
[:c {:optional false} string?]]
{:type "object"
:properties {:a {:type "string"}
:b {:type "string"}
:c {:type "string"}}
:required [:a :c]}]
[[:map
[:x :int]
[::m/default [:map-of :int :int]]]
{:type "object"
:properties {:x {:type "integer"}}
:required [:x]
:additionalProperties {:type "integer"}}]
[[:map
[:x :int]
[::m/default [:fn {:json-schema/default {:x 1}, :gen/gen (gen/return {})} map?]]]
{:type "object"
:properties {:x {:type "integer"}}
:required [:x]
:default {:x 1}}]
[[:map
[:x :int]
[::m/default [:map
[:y :int]
[::m/default [:map
[:z :int]
[::m/default [:map-of :int :int]]]]]]]
{:type "object",
:additionalProperties {:type "integer"},
:properties {:x {:type "integer"}
:y {:type "integer"}
:z {:type "integer"}},
:required [:x :y :z]}]
[[:multi {:dispatch :type
:decode/string '(fn [x] (update x :type keyword))}
[:sized [:map {:gen/fmap '#(assoc % :type :sized)} [:type keyword?] [:size int?]]]
[:human [:map {:gen/fmap '#(assoc % :type :human)} [:type keyword?] [:name string?] [:address [:map [:country keyword?]]]]]
[::m/default :string]]
{:oneOf [{:type "object",
:properties {:type {:type "string"}
:size {:type "integer"}},
:required [:type :size]}
{:type "object",
:properties {:type {:type "string"},
:name {:type "string"},
:address {:type "object"
:properties {:country {:type "string"}}
:required [:country]}},
:required [:type :name :address]}
{:type "string"}]}]
[[:map-of string? string?] {:type "object"
:additionalProperties {:type "string"}}]
[[:vector string?] {:type "array", :items {:type "string"}}]
[[:sequential string?] {:type "array", :items {:type "string"}}]
[[:set string?] {:type "array"
:items {:type "string"}
:uniqueItems true}]
[[:enum 1 2 "3"] {:enum [1 2 "3"]}]
[[:enum 1 2 3] {:type "integer" :enum [1 2 3]}]
[[:enum 1.1 2.2 3.3] {:type "number" :enum [1.1 2.2 3.3]}]
[[:enum "kikka" "kukka"] {:type "string" :enum ["kikka" "kukka"]}]
[[:enum :kikka :kukka] {:type "string" :enum [:kikka :kukka]}]
[[:enum 'kikka 'kukka] {:type "string" :enum ['kikka 'kukka]}]
[[:maybe string?] {:oneOf [{:type "string"} {:type "null"}]}]
[[:tuple string? string?] {:type "array"
:prefixItems [{:type "string"} {:type "string"}]
:items false}]
[[:re "^[a-z]+\\.[a-z]+$"] {:type "string", :pattern "^[a-z]+\\.[a-z]+$"}]
[[:fn {:gen/elements [1]} int?] {}]
[:any {}]
[:some {}]
[:nil {:type "null"}]
[[:string {:min 1, :max 4}] {:type "string", :minLength 1, :maxLength 4}]
[[:int {:min 1, :max 4}] {:type "integer", :minimum 1, :maximum 4}]
[[:float {:min 1, :max 4}] {:type "number", :minimum 1, :maximum 4}]
[[:double {:min 1, :max 4}] {:type "number", :minimum 1, :maximum 4}]
[:keyword {:type "string"}]
[:qualified-keyword {:type "string"}]
[:symbol {:type "string"}]
[:qualified-symbol {:type "string"}]
[:uuid {:type "string", :format "uuid"}]
[[:=> :cat int?] {} :fn]
[[:-> :cat int?] {} :fn]
[[:function [:=> :cat int?]] {} :fn]
[ifn? {}]
[integer? {:type "integer"}]
#?@(:clj [[ratio? {:type "number"}]
[rational? {:type "number"}]]
:cljs [])
;; protocols
[(reify
m/Schema
(-properties [_])
(-parent [_] (reify m/IntoSchema (-type [_]) (-type-properties [_])))
(-form [_])
(-validator [_] int?)
(-walk [t w p o] (m/-outer w t p nil o))
json-schema/JsonSchema
(-accept [_ _ _] {:type "custom"})) {:type "custom"}]
;; type-properties
[malli.core-test/Over6 {:type "integer", :format "int64", :minimum 6}]
[[malli.core-test/Over6 {:json-schema/example 42}] {:type "integer", :format "int64", :minimum 6, :example 42}]])
(deftest json-schema-test
(doseq [[schema json-schema] expectations]
(is (= json-schema (json-schema/transform schema))))
(testing "full override"
(is (= {:type "file"}
(json-schema/transform
[:map {:json-schema {:type "file"}} [:file any?]]))))
(testing "Having all attributes optional in input should not output a required at all even empty. JSON-Schema validation will failed on this
(see http://json-schema.org/understanding-json-schema/reference/object.html#required-properties and
the rule: \"In Draft 4, required must contain at least one string.\")"
(is (= {:type "object",
:properties {:x1 {:title "x", :type "string"},
:x2 {:title "x"},
:x3 {:title "x", :type "string", :default "x"},
:x4 {:title "x-string", :default "x2"},
:x5 {:type "x-string"}}}
(json-schema/transform
[:map
[:x1 {:json-schema/title "x" :optional true} :string]
[:x2 {:json-schema {:title "x"} :optional true} [:string {:json-schema/default "x"}]]
[:x3 {:json-schema/title "x" :optional true} [:string {:json-schema/default "x"}]]
[:x4 {:json-schema/title "x-string" :optional true} [:string {:json-schema {:default "x2"}}]]
[:x5 {:json-schema {:type "x-string"} :optional true} [:string {:json-schema {:default "x"}}]]]))))
(testing "map-entry overrides"
(is (= {:type "object",
:properties {:x1 {:title "x", :type "string"},
:x2 {:title "x"},
:x3 {:title "x", :type "string", :default "x"},
:x4 {:title "x-string", :default "x2"},
:x5 {:type "x-string"}},
:required [:x1 :x2 :x3 :x4 :x5]}
(json-schema/transform
[:map
[:x1 {:json-schema/title "x"} :string]
[:x2 {:json-schema {:title "x"}} [:string {:json-schema/default "x"}]]
[:x3 {:json-schema/title "x"} [:string {:json-schema/default "x"}]]
[:x4 {:json-schema/title "x-string"} [:string {:json-schema {:default "x2"}}]]
[:x5 {:json-schema {:type "x-string"}} [:string {:json-schema {:default "x"}}]]]))))
(testing "with properties"
(is (= {:allOf [{:type "integer"}]
:title "age"
:description "blabla"
:default 42}
(json-schema/transform
[:and {:title "age"
:description "blabla"
:default 42} int?])))
(is (= {:allOf [{:type "integer"}]
:title "age2"
:description "blabla2"
:default 422
:example 422}
(json-schema/transform
[:and {:title "age"
:json-schema/title "age2"
:description "blabla"
:json-schema/description "blabla2"
:default 42
:json-schema/default 422
:json-schema/example 422} int?])))))
(deftest util-schemas-test
(let [registry (merge (m/default-schemas) (mu/schemas))]
(testing "merge"
(is (= {:title "merge",
:type "object",
:properties {:x {:type "integer", :example 42},
:y {:type "integer"},
:z {:type "integer"}},
:required [:x :y :z]}
(json-schema/transform
[:merge {:title "merge"}
[:map [:x {:json-schema/example 42} int?] [:y int?]]
[:map [:z int?]]]
{:registry registry}))))
(testing "union"
(is (= {:title "union",
:type "object",
:properties {:x {:anyOf [{:type "integer"} {:type "string"}]}
:y {:type "integer"}},
:required [:x :y]}
(json-schema/transform
[:union {:title "union"}
[:map [:x int?] [:y int?]]
[:map [:x string?]]]
{:registry registry}))))
(testing "select-keys"
(is (= {:title "select-keys"
:type "object"
:properties {:x {:type "integer"}}
:required [:x]}
(json-schema/transform
[:select-keys {:title "select-keys"}
[:map [:x int?] [:y int?]]
[:x]]
{:registry registry}))))))
(deftest references-test
(testing "absolute doc root definitions are created for ref schemas"
(is (= {:$ref "#/definitions/Order",
:definitions {"Country" {:type "object",
:properties {:name {:type "string"
:enum [:FI :PO]},
:neighbors {:type "array"
:items {:$ref "#/definitions/Country"}}},
:required [:name :neighbors]},
"Burger" {:type "object",
:properties {:name {:type "string"},
:description {:type "string"},
:origin {:oneOf [{:$ref "#/definitions/Country"} {:type "null"}]},
:price {:type "integer"
:minimum 1}},
:required [:name :origin :price]},
"OrderLine" {:type "object",
:properties {:burger {:$ref "#/definitions/Burger"},
:amount {:type "integer"}},
:required [:burger :amount]},
"Order" {:type "object",
:properties {:lines {:type "array"
:items {:$ref "#/definitions/OrderLine"}},
:delivery {:type "object",
:properties {:delivered {:type "boolean"},
:address {:type "object",
:properties {:street {:type "string"},
:zip {:type "integer"},
:country {:$ref "#/definitions/Country"}},
:required [:street :zip :country]}},
:required [:delivered :address]}},
:required [:lines :delivery]}}}
(json-schema/transform
[:schema
{:registry {"Country" [:map
[:name [:enum :FI :PO]]
[:neighbors [:vector [:ref "Country"]]]]
"Burger" [:map
[:name string?]
[:description {:optional true} string?]
[:origin [:maybe "Country"]]
[:price pos-int?]]
"OrderLine" [:map
[:burger "Burger"]
[:amount int?]]
"Order" [:map
[:lines [:vector "OrderLine"]]
[:delivery [:map
[:delivered boolean?]
[:address [:map
[:street string?]
[:zip int?]
[:country "Country"]]]]]]}}
"Order"]))))
(testing "circular definitions are not created"
(is (= {:$ref "#/definitions/Foo", :definitions {"Foo" {:type "integer"}}}
(json-schema/transform
[:schema {:registry {"Foo" :int}} "Foo"]))))
(testing "circular definitions are not created for closed schemas"
(is (= {:$ref "#/definitions/Foo", :definitions {"Foo" {:type "integer"}}}
(json-schema/transform
(mu/closed-schema [:schema {:registry {"Foo" :int}} "Foo"])))))
(testing "definition path can be changed"
(is (= {:type "object"
:properties {:foo {:$ref "#/foo/bar/Foo"}}
:required [:foo]
:definitions {"Foo" {:type "integer"}}}
(json-schema/transform
[:schema {:registry {"Foo" :int}} [:map [:foo "Foo"]]]
{:malli.json-schema/definitions-path "#/foo/bar/"})))))
(deftest mutual-recursion-test
(is (= {:$ref "#/definitions/Foo"
:definitions {"Bar" {:$ref "#/definitions/Foo"}
"Foo" {:items {:$ref "#/definitions/Bar"} :type "array"}}}
(json-schema/transform [:schema {:registry {"Foo" [:vector [:schema "Bar"]] ;; NB! :schema instead of :ref
"Bar" [:ref "Foo"]}}
"Foo"])))
(is (= {:$ref "#/definitions/Foo"
:definitions {"Bar" {:$ref "#/definitions/Foo"}
"Foo" {:items {:$ref "#/definitions/Bar"} :type "array"}}}
(json-schema/transform [:schema {:registry {"Foo" [:vector [:ref "Bar"]]
"Bar" [:ref "Foo"]}}
"Foo"])))
(is (= {:$ref "#/definitions/Bar",
:definitions {"Bar" {:$ref "#/definitions/Foo"},
"Foo" {:items {:$ref "#/definitions/Bar"}, :type "array"}}}
(json-schema/transform [:schema {:registry {"Foo" [:vector [:ref "Bar"]]
"Bar" [:ref "Foo"]}}
"Bar"]))))
(deftest function-schema-test
(is (= {} (json-schema/transform [:=> [:cat int? int?] int?]))))
(deftest additional-properties-test
(is (= {:type "object"
:properties {:name {:type "string"}}
:required [:name]
:additionalProperties false}
(json-schema/transform [:map {:closed true} [:name :string]]))))
(def UserId :string)
(def User
[:map {:registry {:a.b/c :double
:a/b.c :double
::location [:tuple :a.b/c :a/b.c]
`description :string}}
[:id #'UserId]
::location
`description
[:friends {:optional true} [:set [:ref #'User]]]])
(deftest ref-test
(is (= {:type "object"
:properties {:id {:$ref "#/definitions/malli.json-schema-test.UserId"},
::location {:$ref "#/definitions/malli.json-schema-test.location"},
`description {:$ref "#/definitions/malli.json-schema-test.description"},
:friends {:type "array", :items {:$ref "#/definitions/malli.json-schema-test.User"}, :uniqueItems true}},
:required [:id :malli.json-schema-test/location `description],
:definitions {"a..b.c" {:type "number"}
"a.b.c" {:type "number"}
"malli.json-schema-test.UserId" {:type "string"},
"malli.json-schema-test.location" {:type "array",
:prefixItems [{:$ref "#/definitions/a.b.c"}
{:$ref "#/definitions/a..b.c"}],
:items false},
"malli.json-schema-test.description" {:type "string"},
"malli.json-schema-test.User" {:type "object",
:properties {:id {:$ref "#/definitions/malli.json-schema-test.UserId"},
::location {:$ref "#/definitions/malli.json-schema-test.location"},
`description {:$ref "#/definitions/malli.json-schema-test.description"},
:friends {:type "array",
:items {:$ref "#/definitions/malli.json-schema-test.User"},
:uniqueItems true}},
:required [:id ::location `description]}}}
(json-schema/transform User))))
(deftest registry-test
(is (= {:properties {:s {:$ref "#/definitions/malli.json-schema-test.foo"}} :required [:s] :type "object"
:definitions {"malli.json-schema-test.foo" {:type "string"}}}
(json-schema/transform [:map {:registry {::foo :string}} [:s ::foo]])))
(is (= {:properties {:s {:$ref "#/definitions/malli.json-schema-test.foo"}} :required [:s] :type "object"
:definitions {"malli.json-schema-test.foo" {:type "string"}}}
(json-schema/transform [:map [:s ::foo]] {:registry (merge (m/default-schemas) {::foo :string})})))
(is (= {:properties {:s {:$ref "#/definitions/malli.json-schema-test.foo"}} :required [:s] :type "object"
:definitions {"malli.json-schema-test.foo" {:type "string"}}}
(json-schema/transform [:map [:s [:schema ::foo]]] {:registry (merge (m/default-schemas) {::foo :string})})))
(is (= {:properties {:s {:$ref "#/definitions/malli.json-schema-test.foo"}} :required [:s] :type "object"
:definitions {"malli.json-schema-test.foo" {:type "string"}} }
(json-schema/transform [:map [:s [:ref ::foo]]] {:registry (merge (m/default-schemas) {::foo :string})}))))
+311
View File
@@ -0,0 +1,311 @@
(ns malli.parser-test
(:require [clojure.string :as str]
[clojure.test :refer [are deftest is testing]]
[clojure.test.check.generators :as gen]
[clojure.walk :as walk]
[malli.core :as m]
[malli.edn :as edn]
[malli.generator :as mg]
[malli.error :as me]
[malli.impl.util :as miu]
[malli.registry :as mr]
[malli.transform :as mt]
[malli.util :as mu]
#?(:clj [malli.test-macros :refer [when-env]]))
#?(:clj (:import (clojure.lang IFn PersistentArrayMap PersistentHashMap))
:cljs (:require-macros [malli.test-macros :refer [when-env]])))
(defn simple-parser? [s] (boolean (:simple-parser (m/-parser-info (m/schema s) nil))))
(def inheriting-parser-templates
"Schema templates which have simple parsers iff ::HOLE has a simple parser.
Should also be generatable for any ::HOLE and have high likelihood of (un)parsing
to a different value than its input if transforming."
[::HOLE
[:maybe ::HOLE]
[:schema ::HOLE]
[:schema {:registry {::a ::HOLE}} ::a]
[:schema {:registry {::a ::HOLE}} [:ref ::a]]
[:schema {:registry {::a [:ref ::b] ::b ::HOLE}} [:ref ::a]]
[:tuple ::HOLE]
[:tuple ::HOLE :any]
[:vector ::HOLE]
[:set ::HOLE]
[:seqable ::HOLE]
[:map [:foo ::HOLE]]
[:map [:foo {:optional true} ::HOLE]]
[:map [:foo ::HOLE] [:bar :int]]
[:and ::HOLE] ;; generator will fail if :any is first
[:and ::HOLE :any]
[:and ::HOLE :any :any]
[:or ::HOLE] ;; parser will always be identical if :any is first
[:or ::HOLE :any]
[:map-of ::HOLE :any]
[:map-of :any ::HOLE]
[:map-of ::HOLE ::HOLE]])
(def simple-parser-templates
"Schema templates which have simple parsers for any value of ::HOLE."
[[:and {:parse/transforming-child 1} ::HOLE :any]
[:and {:parse/transforming-child :none} ::HOLE :any]
[:every ::HOLE]
[:-> ::HOLE]
[:function [:-> ::HOLE]]])
(def transforming-parser-templates
"Schema templates which have transforming parsers for any value of ::HOLE."
[[:multi {:dispatch #'any?} [true ::HOLE]]
[:multi {:dispatch #'boolean} [true :any] [false ::HOLE]]
[:multi {:dispatch #'boolean} [true ::HOLE] [false :any]]
[:andn [0 ::HOLE]]
[:andn [0 ::HOLE] [1 :any]] ;; generator will fail if :any is first
[:orn [0 ::HOLE]]
[:orn [0 ::HOLE] [1 :any]]
[:orn [0 :any] [1 ::HOLE]]
[:orn [0 ::HOLE] [1 ::HOLE]]])
(def simple-parser-schemas
"Schemas with simple parsers."
[:any
[:and :any]
:int
#'map?
:tuple
[:fn {:gen/schema :any} #'any?]
[:= 42] [:enum 42] [:not= 42] [:< 5] [:> 5] [:<= 5] [:>= 5]
#?@(:cljs [] :default [[:re #""]]) ;; no generator in cljs
:nil
:qualified-symbol
:uuid
[:not [:= (random-uuid)]] ;; generator is too unreliable to nest
:some])
(def transforming-parser-schemas
"Schemas with transforming parsers."
[[:andn [:any :any]]
[:catn [:any :any]]
[:seqable [:catn [:any :any]]]
[:multi {:dispatch #'any?} [true :any]]])
(defn ensure-parser-type [expected-simple s]
#?(:bb nil ;; test.chuck doesn't work in bb
:default (let [s (m/schema s)
parse (m/parser s)
unparse (m/parser s)]
(if expected-simple
(doseq [g (mg/sample s {:seed 0})]
(testing (pr-str g)
(let [p (parse g)]
(is (identical? g p))
(is (identical? g (unparse p))))))
(is (some (fn [g]
(let [p (parse g)]
(and (not (identical? g p))
(not (identical? g (unparse p))))))
(mg/sample s {:seed 0})))))))
(deftest parser-info-test
;; should really be in simple-parser-templates but :not has an unreliable generator
(testing ":not is simple"
(is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas)))
(ensure-parser-type true [:not [:= (random-uuid)]])
(ensure-parser-type true [:not [:andn [:tag [:= (random-uuid)]]]]))
(testing ":multi is transforming"
(is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas)))
(ensure-parser-type true [:not [:andn [:any [:= (random-uuid)]]]]))
(let [d (m/default-schemas)]
(doseq [[hole hold-simple] (concat (map vector simple-parser-schemas (repeat true))
(map vector transforming-parser-schemas (repeat false)))
:let [_ (testing (pr-str hole)
(is (= hold-simple (simple-parser? hole))))]
[template expected-simple] (concat (map vector simple-parser-templates (repeat true))
(map vector transforming-parser-templates (repeat false))
(map vector inheriting-parser-templates (repeat hold-simple)))
:let [s (testing {:template template :hole hole}
(is (m/schema template {:registry (assoc d ::HOLE (m/schema hole))})))]]
(testing (pr-str (list 'm/schema template
{:registry (list 'assoc (list 'm/default-schemas)
(symbol "::HOLE") (list 'm/schema hole))}))
(is (= expected-simple (simple-parser? s)))
(ensure-parser-type expected-simple s)))))
(deftest and-complex-parser-test
(is (= {} (m/parse [:and :map [:fn map?]] {})))
(is (= {} (m/parse [:and [:fn map?] :map] {})))
(is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:orn [:left :int] [:right :int]] [:fn number?]] 1)))
(is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:fn number?] [:orn [:left :int] [:right :int]]] 1)))
(is (= 1 (m/parse [:and {:parse/transforming-child :none} [:fn number?] [:orn [:left :int] [:right :int]]] 1)))
(is (= 1 (m/parse [:and :int [:or :int :boolean]] 1)))
(is (= 1 (m/parse [:and [:or :int :boolean] :int] 1)))
(is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and :int [:orn [:int :int] [:boolean :boolean]]] 1)))
(is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:orn [:int :int] [:boolean :boolean]] :int] 1)))
(is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:and [:orn [:int :int] [:boolean :boolean]] :int] :int] 1)))
(is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}}
(m/parse [:and
[:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]]
:int] 1)))
(is (= 1
(m/parse [:and
{:parse/transforming-child :none}
[:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]]
[:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]
1)))
(is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}}
(m/parse [:and
{:parse/transforming-child 0}
[:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]]
[:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]
1)))
(is (= #malli.core.Tag{:key :r, :value #malli.core.Tag{:key :int, :value 1}}
(m/parse [:and
{:parse/transforming-child 1}
[:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]]
[:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]
1)))
(let [s [:and [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] :int]]
(is (= 1 (->> 1 (m/parse s) (m/unparse s)))))
(let [s [:and
{:parse/transforming-child 1}
[:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]]
[:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]]
(is (= 1 (->> 1 (m/parse s) (m/unparse s)))))
(is (m/parser [:and [:map] [:map]]))
(is (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map]]))
(is (m/parser [:and [:map] [:map [:left [:orn [:one :int]]]]]))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli\.core/and-schema-multiple-transforming-parsers"
(m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]])))
(is (-> (m/schema [:vector :int]) (m/-parser-info nil) :simple-parser))
(is (-> (m/schema [:vector [:orn [:one :int]]]) (m/-parser-info nil) :simple-parser not))
(is (= #malli.core.Tags{:values {"a" 3, "b" :x}}
(m/parse [:and [:catn ["a" :int] ["b" :keyword]]
[:fn vector?]]
[3 :x])))
(let [s [:and [:catn ["a" :int] ["b" :keyword]]
[:fn vector?]]
res (->> [3 :x]
(m/parse s)
(m/unparse s))]
(is (= [3 :x] res))
(is (m/validate s res)))
(let [s [:and [:catn ["a" :int] ["b" :keyword]]
[:vector :any]]
res (->> [3 :x]
(m/parse s)
(m/unparse s))]
(is (= [3 :x] res))
(is (m/validate s res)))
(let [s [:and [:catn ["a" :int] ["b" :keyword]]
[:sequential :any]]
res (->> [3 :x]
(m/parse s)
(m/unparse s))]
(is (= [3 :x] res))
(is (m/validate s res)))
(let [s [:and [:catn ["a" :int] ["b" :keyword]]
[:tuple :any :any]]
res (->> [3 :x]
(m/parse s)
(m/unparse s))]
(is (= [3 :x] res))
(is (m/validate s res))))
(def cyclic-simple-parsers
[[:schema {:registry
{::Name [:or :keyword :string]
::Value [:or
number?
:string
:boolean
:nil
:keyword
[:sequential [:ref ::Value]]
[:map-of [:ref ::Name] [:ref ::Value]]]
::Arguments [:map-of [:ref ::Name] [:ref ::Value]]}}
::Arguments]
[:schema {:registry
{::Value [:sequential [:ref ::Value]]}}
::Value]
;; equivalent to :nil
[:schema {:registry {::a [:maybe [:ref ::a]]}}
[:ref ::a]]
;; inner ::a shadows outer ::a
[:schema {:registry {::a [:schema {:registry {::a [:= 42]}} [:ref ::a]]}}
[:ref ::a]]
[:schema {:registry {::a [:ref ::b] ;; (1)
::b [:schema {:registry {::b [:= 42]}}
;; (2)
[:ref ::b]]}}
[:ref ::a]]
;; it's insufficient to identify refs just by their expansion. here, ::a
;; expands to [:ref ::b] twice at (1) and (2), so it looks like a recursion point, except
;; they are different ::b's!
[:schema {:registry {::a [:ref ::b] ;; (1)
::b [:schema {:registry {::a [:ref ::b] ;; (2)
::b [:= 42]}}
[:ref ::a]]}}
[:ref ::a]]
;; if the outer ::a shadowed the inner one, it would be equivalent to
;; [:maybe :never] == [:maybe [:maybe :never]] == ..., which is just :nil
[:schema {:registry {::a [:schema {:registry {::a :int}} [:maybe [:ref ::a]]]}}
[:ref ::a]]
[:schema
{:registry {::outer [:schema {:registry {::outer :int
::inner [:ref ::outer]}}
[:ref ::inner]]}}
[:ref ::outer]]
[:schema {:registry {::cons [:or :nil [:tuple pos-int? [:ref ::cons]]]}}
[:ref ::cons]]])
(def cyclic-transforming-parsers
[[:schema {:registry
{::Name [:orn [:k :keyword] [:s :string]]
::Value [:or
number?
:string
:boolean
:nil
:keyword
[:sequential [:ref ::Value]]
[:map-of [:ref ::Name] [:ref ::Value]]]
::Arguments [:map-of [:ref ::Name] [:ref ::Value]]}}
::Arguments]
[:schema {:registry
{::Value [:sequential [:orn [:a [:ref ::Value]]]]}}
::Value]])
(deftest cycle-detection-test
(doseq [s (map m/schema cyclic-simple-parsers)]
(testing (pr-str (m/form s))
(is (m/parser s))
(is (m/unparser s))
(is (true? (:simple-parser (m/-parser-info s nil))))))
(doseq [s (map m/schema cyclic-transforming-parsers)]
(testing (pr-str (m/form s))
(is (m/parser s))
(is (m/unparser s))
(is (not (:simple-parser (m/-parser-info s nil)))))))
(def infinite-parsers
"Schemas whose parsers diverge or are unsatisfiable.
-parser-info may infer these as simple, or diverge also."
[[:schema {:registry {::a [:ref ::a]}} [:ref ::a]]
[:schema {:registry {::a [:tuple [:ref ::a]]}}
[:ref ::a]]
;; the scopes at (1) and (2) are different, so no recursion is detected between them.
;; instead, the [:ref ::a] at (2) is the recursion point with itself.
[:schema {:registry {::a [:schema {:registry {::b [:= true]}}
;; (2)
[:or [:ref ::a] [:ref ::b]]]}}
[:schema {:registry {::b [:= false]}}
;; (1)
[:or [:ref ::a] [:ref ::b]]]]])
(deftest infinite-parser-test
(doseq [s (map m/schema infinite-parsers)]
(is (m/parser s))
(is (m/unparser s))
(is (try (:simple-parser (m/-parser-info s nil))
(catch #?(:bb Throwable :clj StackOverflowError :cljs js/Error) _ true)))))
+37
View File
@@ -0,0 +1,37 @@
(ns malli.plantuml-test
(:require [clojure.string :as str]
[clojure.test :refer [deftest is]]
[malli.plantuml :as plantuml]))
(defn trimmed= [s1 s2]
(letfn [(trim [x] (str/trim (str/replace x #"\s+" " ")))]
(= (trim s1) (trim s2))))
(deftest transform-test
(is (trimmed=
"@startuml
entity :malli.dot/schema {
[:enum {:title \"enum\"} \"S\" \"M\" \"L\"]
}
@enduml"
(plantuml/transform
[:enum {:title "enum"} "S" "M" "L"])))
(is (trimmed=
"@startuml
entity :malli.dot/schema {
:x :string
}
@enduml"
(plantuml/transform
[:map {:x 1}
[:x {:x 1} :string]])))
(is (trimmed=
"@startuml
entity :malli.dot/schema {
[:and int? [:< 100]]
}
@enduml"
(plantuml/transform [:and int? [:< 100]]))))
+164
View File
@@ -0,0 +1,164 @@
(ns malli.provider-test
(:require [clojure.test :refer [deftest is]]
[malli.core :as m]
[malli.provider :as mp]
[malli.transform :as mt])
#?(:clj (:import (java.util UUID Date))))
(def expectations
[[:int [1 2 3]]
[:keyword [:kikka :kukka]]
[:qualified-keyword [::kikka ::kukka]]
[:uuid [#?(:clj (UUID/randomUUID) :cljs (random-uuid))]]
[inst? [#?(:clj (Date.) :cljs (js/Date.))]]
[:any []]
[[:vector :keyword] [[:kikka] [:kukka :kakka]]]
[[:sequential :symbol] [(seq ['kikka]) (seq ['kikka 'kakka])]]
[[:set :string] [#{"a" "b"} #{"c"}]]
[[:vector [:sequential [:set :int]]] [[(list #{1})]]]
[[:vector :any] [[]]]
[[:maybe :int] [1 nil 2 3]]
[[:maybe :some] [1 nil 2 "some"]]
[[:maybe [:map [:x :int]]] [{:x 1} nil]]
[[:maybe [:or [:map [:x :int]] :string]] [{:x 1} nil "1"]]
;; normal maps without type-hint
[[:map
[:a [:map
[:b :int]
[:c :int]]]
[:b [:map
[:b :int]
[:c :int]]]
[:c [:map
[:b :int]]]
[:d :nil]]
[{:a {:b 1, :c 2}
:b {:b 2, :c 1}
:c {:b 3}
:d nil}]]
;; :map-of type-hint
[[:map-of :keyword [:maybe [:map
[:b :int]
[:c {:optional true} :int]]]]
[^{::mp/hint :map-of}
{:a {:b 1, :c 2}
:b {:b 2, :c 1}
:c {:b 3}
:d nil}]]
;; too few samples for :map-of
[[:map
["1" [:map [:name :string]]]
["2" [:map [:name :string]]]]
[{"1" {:name "1"}
"2" {:name "2"}}]]
;; explicit sample count for :map-of
[[:map-of :string [:map [:name :string]]]
[{"1" {:name "1"}}
{"2" {:name "2"}}]
{::mp/map-of-threshold 2}]
;; tuple-like without options
[[:vector :some]
[[1 "kikka" true]
[2 "kukka" true]
[3 "kakka" false]]]
;; tuple-like with threshold not reached
[[:vector :some]
[[1 "kikka" true]
[2 "kukka" true]
[3 "kakka" false]]
{::mp/tuple-threshold 4}]
;; tuple-like with threshold reached
[[:tuple :int :string :boolean]
[[1 "kikka" true]
[2 "kukka" true]
[3 "kakka" false]]
{::mp/tuple-threshold 3}]
;; tuple-like with non-coherent data
[[:vector :some]
[[1 "kikka" true]
[2 "kukka" true]
[3 "kakka" "true"]]]
;; a homogenous hinted tuple
[[:tuple :int :string :boolean]
[^{::mp/hint :tuple} [1 "kikka" true]
[2 "kukka" true]]]
;; a hererogenous hinted tuple
[[:tuple :int :string :some]
[^{::mp/hint :tuple} [1 "kikka" true]
[2 "kukka" "true"]]]
;; invalid hinted tuple
[[:vector :some]
[^{::mp/hint :tuple} [1 "kikka" true]
[2 "kukka" true "invalid tuple"]]]
;; value-decoders
[[:map [:id :string]]
[{:id "caa71a26-5fe1-11ec-bf63-0242ac130002"}
{:id "8aadbf5e-5fe3-11ec-bf63-0242ac130002"}]]
[[:map [:id :uuid]]
[{:id "caa71a26-5fe1-11ec-bf63-0242ac130002"}
{:id "8aadbf5e-5fe3-11ec-bf63-0242ac130002"}]
{::mp/value-decoders {:string {:uuid mt/-string->uuid}}}]
[[:map-of :uuid [:map [:id :uuid]]]
[{"0423191a-5fee-11ec-bf63-0242ac130002" {:id "0423191a-5fee-11ec-bf63-0242ac130002"}}
{"09e59de6-5fee-11ec-bf63-0242ac130002" {:id "09e59de6-5fee-11ec-bf63-0242ac130002"}}
{"15511020-5fee-11ec-bf63-0242ac130002" {:id "15511020-5fee-11ec-bf63-0242ac130002"}}]
{::mp/value-decoders {:string {:uuid mt/-string->uuid}}
::mp/map-of-threshold 3}]
[[:map-of inst? :string]
[{"1901-03-02T22:20:11.000Z" "123"}
{"1902-04-03T22:20:11.000Z" "234"}
{"1904-06-05T22:20:11.000Z" "456"}]
{::mp/value-decoders {:string {'inst? mt/-string->date}}
::mp/map-of-threshold 3}]
;; value-hints
[[:map [:name :string] [:gender [:enum "male" "female"]]]
[{:name "Tommi", :gender (mp/-hinted "male" :enum)}
{:name (mp/-hinted "Tiina" :string), :gender "female"}]]
[[:map
[:speed #?(:clj :float, :cljs :double)]
[:position [:vector #?(:clj :float, :cljs :double)]]]
[{:speed (float 1.5)
:position [(float 300.33) (float 663.66)]}]]
[[:map
[:id :string]
[:tags [:set :keyword]]
[:address
[:map
[:street :string]
[:city :string]
[:zip :int]
[:lonlat [:vector :double]]]]
[:description {:optional true} :string]]
[{:id "Lillan"
:tags #{:artesan :coffee :hotel}
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip 33100
:lonlat [61.4858322, 23.7854658]}}
{:id "Huber",
:description "Beefy place"
:tags #{:beef :wine :beer}
:address {:street "Aleksis Kiven katu 13"
:city "Tampere"
:zip 33200
:lonlat [61.4963599 23.7604916]}}]]])
(deftest provider-test
(doseq [[schema samples options] expectations]
(is (= (m/form schema) (m/form (mp/provide samples options))))))
+133
View File
@@ -0,0 +1,133 @@
(ns malli.registry-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.registry :as mr]))
(deftest mutable-test
(let [registry* (atom (m/default-schemas))
registry (mr/mutable-registry registry*)
register! (fn [t ?s] (swap! registry* assoc t ?s))]
(testing "default registy"
(is (thrown? #?(:clj Exception, :cljs js/Error) (m/validate :str "kikka" {:registry registry})))
(register! :str (m/-string-schema))
(is (true? (m/validate :str "kikka" {:registry registry}))))
(register! ::int-pair (m/schema [:tuple :int :int]))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#?(:clj #":malli\.core/infinitely-expanding-schema"
:cljs #":malli\.core/invalid-schema")
(m/schema [::int-pair {:foo :bar}] {:registry registry})))))
(deftest composite-test
(let [registry* (atom {})
register! (fn [t ?s] (swap! registry* assoc t ?s))
registry (mr/composite-registry
{:map (m/-map-schema)}
(mr/mutable-registry registry*)
(mr/dynamic-registry))]
;; register
(register! :maybe (m/-maybe-schema))
;; use
(binding [mr/*registry* {:string (m/-string-schema)}]
(is (true? (m/validate
[:map [:maybe [:maybe :string]]]
{:maybe "sheep"}
{:registry registry})))
(is (= #{:string :map :maybe} (-> registry (mr/-schemas) (keys) (set)))))))
(deftest lazy-registry-test
(let [loads (atom [])
registry (mr/lazy-registry
(m/default-schemas)
(fn [type registry]
(let [lookup {"AWS::ApiGateway::UsagePlan" [:map {:closed true}
[:Type [:= "AWS::ApiGateway::UsagePlan"]]
[:Description {:optional true} string?]
[:UsagePlanName {:optional true} string?]]
"AWS::AppSync::ApiKey" [:map {:closed true}
[:Type [:= "AWS::AppSync::ApiKey"]]
[:ApiId string?]
[:Description {:optional true} string?]]}
schema (some-> type lookup (m/schema {:registry registry}))]
(swap! loads conj type)
schema)))
new-loads! #(first (reset-vals! loads []))
CloudFormation (m/schema [:multi {:lazy-refs true, :dispatch :Type}
"AWS::ApiGateway::Stage"
"AWS::ApiGateway::UsagePlan"
"AWS::AppSync::ApiKey"]
{:registry registry})]
(testing "nothing is loaded"
(is (= [] (new-loads!))))
(testing "validating a schema pulls schema"
(let [f (m/validator CloudFormation)]
(is (= [] (new-loads!)))
(is (f {:Type "AWS::AppSync::ApiKey"
:ApiId "123"
:Description "apkey"}))
(is (= ["AWS::AppSync::ApiKey"] (new-loads!)))))
(testing "pulling more"
(let [f (m/validator CloudFormation)]
(is (= [] (new-loads!)))
(is (f {:Type "AWS::ApiGateway::UsagePlan"}))
(is (= ["AWS::ApiGateway::UsagePlan"] (new-loads!)))))))
(deftest recursive-lazy-registry-test
(let [loads (atom [])
registry (mr/lazy-registry
(m/default-schemas)
(fn [type registry]
(let [lookup {::List [:multi {:lazy-refs true :dispatch :op}
::Nil
::Cons]
::Nil [:map [:op [:enum ::Nil]]]
::Cons [:map [:op [:enum ::Cons]] [:car :any] [:cdr ::List]]}
schema (some-> type lookup (m/schema {:registry registry}))]
(swap! loads conj type)
schema)))
new-loads! #(first (reset-vals! loads []))
List (m/schema ::List {:registry registry})]
(testing "::Cons and ::Nil are lazily loaded"
(is (= [::List] (new-loads!))))
(testing "the first two levels are lazily pulled, then cached"
(let [f (m/validator List)]
(testing "nothing is pulled to create validator"
(is (= [] (new-loads!))))
(testing "validating the first ::Nil the first time pulls"
(is (f {:op ::Nil}))
(is (= [::Nil] (new-loads!))))
(testing "validating the first ::Nil the second time is cached"
(is (f {:op ::Nil}))
(is (= [] (new-loads!))))
(testing "validating the first ::Cons the first time pulls"
(is (f {:op ::Cons
:car "first"
:cdr {:op ::Nil}}))
(is (= [::Cons] (new-loads!))))
(testing "validating the first ::Cons the second time is cached"
(is (f {:op ::Cons
:car "first"
:cdr {:op ::Nil}}))
(is (= [] (new-loads!))))
(testing "not further pulls are needed"
(testing "for nested good values"
(is (f (nth (iterate
(fn [x]
{:op ::Cons
:car "elem"
:cdr x})
{:op ::Nil})
10)))
(is (= [] (new-loads!))))
(testing "for nested bad values"
(is (not (f {:op ::Cons
:car "first"
:cdr {:op ::Cons
:car "second"
:cdr "junk"}})))
(is (= [] (new-loads!)))))))))
+567
View File
@@ -0,0 +1,567 @@
(ns malli.swagger-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.core-test]
[malli.swagger :as swagger]
[malli.util :as mu]))
(def expectations
[;; predicates
[pos-int? {:type "integer", :format "int64", :minimum 1}]
[float? {:type "number" :format "float"}]
;; comparators
[[:> 6] {:type "number", :exclusiveMinimum 6}]
[[:>= 6] {:type "number", :minimum 6}]
[[:< 6] {:type "number", :exclusiveMaximum 6}]
[[:<= 6] {:type "number", :maximum 6}]
;; base
[[:not string?] {:x-not {:type "string"}}]
[[:and int? pos-int?] {:type "integer"
:format "int64"
:x-allOf [{:type "integer", :format "int64"}
{:type "integer", :format "int64", :minimum 1}]}]
[[:or int? string?] {:type "integer"
:format "int64"
:x-anyOf [{:type "integer", :format "int64"}
{:type "string"}]}]
[[:or int? :nil] {:type "integer"
:format "int64"
:x-anyOf [{:type "integer", :format "int64"}
{:type "null"}]}]
[[:or :nil int?] {:type "integer"
:format "int64"
:x-anyOf [{:type "null"}
{:type "integer", :format "int64"}]}]
[[:or [:or :nil int?] [:or :nil int?]] {:type "integer"
:format "int64"
:x-anyOf [{:type "integer", :format "int64", :x-anyOf [{:type "null"} {:type "integer", :format "int64"}]}
{:type "integer", :format "int64", :x-anyOf [{:type "null"} {:type "integer", :format "int64"}]}]}]
[[:and int? :nil] {:type "integer"
:format "int64"
:x-allOf [{:type "integer", :format "int64"}
{:type "null"}]}]
[[:and :nil int?] {:type "integer"
:format "int64"
:x-allOf [{:type "null"}
{:type "integer", :format "int64"}]}]
[[:multi {:dispatch :whatever}
[:a int?]
[:b :nil]]
{:type "integer"
:format "int64"
:x-anyOf [{:type "integer", :format "int64"}
{:type "null"}]}]
[[:multi {:dispatch :whatever}
[:a :nil]
[:b int?]]
{:type "integer"
:format "int64"
:x-anyOf [{:type "null"}
{:type "integer", :format "int64"}]}]
[[:map
[:a string?]
[:b {:optional true} string?]
[:c {:optional false} string?]] {:type "object"
:properties {:a {:type "string"}
:b {:type "string"}
:c {:type "string"}}
:required [:a :c]}]
[[:multi {:dispatch :type
:decode/string '(fn [x] (update x :type keyword))}
[:sized [:map [:type keyword?] [:size int?]]]
[:human [:map [:type keyword?] [:name string?] [:address [:map [:country keyword?]]]]]]
{:type "object"
:properties {:type {:type "string"}
:size {:type "integer"
:format "int64"}}
:required [:type :size]
:x-anyOf [{:type "object"
:properties {:type {:type "string"}
:size {:type "integer"
:format "int64"}}
:required [:type :size]}
{:type "object"
:properties {:type {:type "string"}
:name {:type "string"}
:address {:type "object"
:properties {:country {:type "string"}}
:required [:country]}}
:required [:type :name :address]}]}]
[[:map-of string? string?] {:type "object"
:additionalProperties {:type "string"}}]
[[:vector string?] {:type "array", :items {:type "string"}}]
[[:sequential string?] {:type "array", :items {:type "string"}}]
[[:set string?] {:type "array"
:items {:type "string"}
:uniqueItems true}]
[[:enum 1 2 "3"] {:enum [1 2 "3"]}]
[[:enum 1 2 3] {:type "integer" :enum [1 2 3]}]
[[:enum 1.1 2.2 3.3] {:type "number" :enum [1.1 2.2 3.3]}]
[[:enum "kikka" "kukka"] {:type "string" :enum ["kikka" "kukka"]}]
[[:enum :kikka :kukka] {:type "string" :enum [:kikka :kukka]}]
[[:maybe string?] {:type "string", :x-nullable true}]
[[:tuple string? string?] {:type "array"
:items {}
:x-items [{:type "string"}
{:type "string"}]}]
[[:re "^[a-z]+\\.[a-z]+$"] {:type "string", :pattern "^[a-z]+\\.[a-z]+$"}]
[[:string {:min 1, :max 4}] {:type "string", :minLength 1, :maxLength 4}]
[[:int {:min 1, :max 4}] {:type "integer", :format "int64", :minimum 1, :maximum 4}]
[[:double {:min 1, :max 4}] {:type "number", :format "double" :minimum 1, :maximum 4}]
[:keyword {:type "string"}]
[:qualified-keyword {:type "string"}]
[:symbol {:type "string"}]
[:qualified-symbol {:type "string"}]
[:uuid {:type "string", :format "uuid"}]
[integer? {:type "integer" :format "int32"}]
#?@(:clj [[ratio? {:type "number"}]
[rational? {:type "number"}]]
:cljs [])
;; protocols
[(reify
m/Schema
(-properties [_])
(-parent [_] (reify m/IntoSchema (-type [_]) (-type-properties [_])))
(-form [_])
(-validator [_] int?)
(-walk [t w p o] (m/-outer w t p nil o))
swagger/SwaggerSchema
(-accept [_ _ _] {:type "custom"})) {:type "custom"}]
;; type-properties
[malli.core-test/Over6 {:type "integer", :format "int64", :minimum 6}]
[[malli.core-test/Over6 {:json-schema/example 42}] {:type "integer", :format "int64", :minimum 6, :example 42}]])
(deftest swagger-test
(doseq [[schema swagger-schema] expectations]
(is (= swagger-schema (swagger/transform schema))))
(testing "full override"
(is (= {:type "file"}
(swagger/transform
[:map {:swagger {:type "file"}} [:file any?]])))
(is (= {:type "file"}
(swagger/transform
[:map {:json-schema {:type "file"}} [:file any?]])))
(is (= {:type "file"}
(swagger/transform
[:map {:swagger {:type "file"}
:json-schema {:type "file2"}} [:file any?]]))))
(testing "map-entry overrides"
(is (= {:type "object"
:properties {:x1 {:title "x", :type "string"}
:x2 {:title "x"}
:x3 {:title "x", :type "string", :default "x"}
:x4 {:title "x-string", :default "x2"}
:x5 {:type "x-string"}}
:required [:x1 :x2 :x3 :x4 :x5]}
(swagger/transform
[:map
[:x1 {:swagger/title "x"} :string]
[:x2 {:swagger {:title "x"}} [:string {:swagger/default "x"}]]
[:x3 {:swagger/title "x"} [:string {:swagger/default "x"}]]
[:x4 {:swagger/title "x-string"} [:string {:swagger {:default "x2"}}]]
[:x5 {:swagger {:type "x-string"}} [:string {:swagger {:default "x"}}]]]))))
(testing "with properties"
(is (= {:title "age"
:type "integer"
:format "int64"
:description "blabla"
:default 42
:x-allOf [{:type "integer", :format "int64"}]}
(swagger/transform
[:and {:title "age"
:description "blabla"
:default 42} int?])))
(is (= {:title "age2"
:type "integer"
:format "int64"
:description "blabla2"
:default 422
:example 422
:x-allOf [{:type "integer", :format "int64"}]}
(swagger/transform
[:and {:title "age"
:json-schema/title "age2"
:description "blabla"
:json-schema/description "blabla2"
:default 42
:json-schema/default 422
:json-schema/example 422} int?])))
(is (= {:title "age3"
:type "integer"
:format "int64"
:description "blabla3"
:default 4222
:example 4222
:x-allOf [{:type "integer", :format "int64"}]}
(swagger/transform
[:and {:title "age"
:json-schema/title "age2"
:swagger/title "age3"
:description "blabla"
:json-schema/description "blabla2"
:swagger/description "blabla3"
:default 42
:json-schema/default 422
:swagger/default 4222
:json-schema/example 422
:swagger/example 4222} int?])))))
(deftest null-base-test
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli\.swagger/non-null-base-needed"
(swagger/transform [:or :nil :nil])))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli\.swagger/non-null-base-needed"
(swagger/transform :nil)))
(is (thrown-with-msg?
#?(:clj Exception, :cljs js/Error)
#":malli\.swagger/non-null-base-needed"
(swagger/transform [:maybe :nil]))))
(deftest util-schemas-test
(let [registry (merge (m/default-schemas) (mu/schemas))]
(testing "merge"
(is (= {:title "merge"
:type "object"
:properties {:x {:type "integer", :format "int64", :example 42}
:y {:type "integer", :format "int64"}
:z {:type "integer", :format "int64"}}
:required [:x :y :z]}
(swagger/transform
[:merge {:title "merge"}
[:map [:x {:swagger/example 42} int?] [:y int?]]
[:map [:z int?]]]
{:registry registry}))))
(testing "union"
(is (= {:title "union"
:type "object"
:properties {:x {:format "int64"
:type "integer"
:x-anyOf [{:format "int64"
:type "integer"}
{:type "string"}]}
:y {:type "integer", :format "int64"}}
:required [:x :y]}
(swagger/transform
[:union {:title "union"}
[:map [:x int?] [:y int?]]
[:map [:x string?]]]
{:registry registry}))))
(testing "select-keys"
(is (= {:title "select-keys"
:type "object"
:properties {:x {:type "integer", :format "int64"}}
:required [:x]}
(swagger/transform
[:select-keys {:title "select-keys"}
[:map [:x int?] [:y int?]]
[:x]]
{:registry registry}))))))
(deftest references-test
(is (= {:$ref "#/definitions/Order"
:definitions {"Country" {:type "object"
:properties {:name {:type "string"
:enum [:FI :PO]}
:neighbors {:type "array"
:items {:$ref "#/definitions/Country"}}}
:required [:name :neighbors]}
"Burger" {:type "object"
:properties {:name {:type "string"}
:description {:type "string"}
:origin {:$ref "#/definitions/Country"
:x-nullable true}
:price {:type "integer"
:format "int64"
:minimum 1}}
:required [:name :origin :price]}
"OrderLine" {:type "object"
:properties {:burger {:$ref "#/definitions/Burger"}
:amount {:type "integer"
:format "int64"}}
:required [:burger :amount]}
"Order" {:type "object"
:properties {:lines {:type "array"
:items {:$ref "#/definitions/OrderLine"}}
:delivery {:type "object"
:properties {:delivered {:type "boolean"}
:address {:type "object"
:properties {:street {:type "string"}
:zip {:type "integer"
:format "int64"}
:country {:$ref "#/definitions/Country"}}
:required [:street :zip :country]}}
:required [:delivered :address]}}
:required [:lines :delivery]}}}
(swagger/transform
[:schema
{:registry {"Country" [:map
[:name [:enum :FI :PO]]
[:neighbors [:vector [:ref "Country"]]]]
"Burger" [:map
[:name string?]
[:description {:optional true} string?]
[:origin [:maybe "Country"]]
[:price pos-int?]]
"OrderLine" [:map
[:burger "Burger"]
[:amount int?]]
"Order" [:map
[:lines [:vector "OrderLine"]]
[:delivery [:map
[:delivered boolean?]
[:address [:map
[:street string?]
[:zip int?]
[:country "Country"]]]]]]}}
"Order"]))))
(def Request [:map-of :keyword :any])
(def QueryB [:string {:min 10}])
(def Query [:map [:a :int] [:b #'QueryB]])
(def SuccessWorked [:= "worked"])
(def Success [:map [:it #'SuccessWorked]])
(deftest swagger-spec-test
(testing "generates swagger for ::parameters and ::responses w/ basic schema"
(is (= {:parameters [{:description ""
:in "body"
:name "body"
:required true
:schema {:properties {:foo {:type "string"}}
:required [:foo] :type "object"}}
{:description ""
:in "query"
:name :a
:required true
:type "string"}
{:description ""
:in "query"
:name :b
:required true
:type "string"}
{:description ""
:in "header"
:name :c
:required true
:type "string"}]
:responses {200 {:description ""
:schema {:properties {:bar {:type "string"}}
:required [:bar], :type "object"}}}}
(swagger/swagger-spec {::swagger/parameters
{:body [:map [:foo :string]]
:query [:map [:a :string] [:b :string]]
:header [:map [:c :string]]}
::swagger/responses
{200 {:schema [:map [:bar :keyword]]}}}))))
(testing "generates swagger for ::parameters w/ basic schema + registry"
(let [registry (merge (m/type-schemas)
{::body [:string {:min 1}]})]
(is (= {:definitions {"malli.swagger-test.body" {:minLength 1, :type "string"}}
:parameters [{:description ""
:in "body"
:name "body"
:required true
:schema {:$ref "#/definitions/malli.swagger-test.body"}}]}
(swagger/swagger-spec {::swagger/parameters
{:body (m/schema ::body
{:registry registry})}})))))
(testing "generates swagger for ::responses w/ basic schema + registry"
(let [registry (merge (m/base-schemas) (m/type-schemas)
{::success [:map-of :keyword :string]
::error [:string {:min 1}]})]
(is (= {:definitions {"malli.swagger-test.error" {:minLength 1, :type "string"}
"malli.swagger-test.success" {:additionalProperties {:type "string"}
:type "object"}}
:responses {200 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.success"}}
400 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.error"}}}}
(swagger/swagger-spec {::swagger/responses
{200 {:schema (m/schema ::success
{:registry registry})}
400 {:schema (m/schema ::error
{:registry registry})}}})))))
(testing "generates swagger for ::parameters and ::responses w/ basic schema + registry"
(let [registry (merge (m/base-schemas) (m/type-schemas) (m/comparator-schemas)
{::req-body [:map-of :keyword :any]
::query-b [:string {:min 10}]
::query [:map [:a :int] [:b ::query-b]]
::success-resp [:map [:it [:= "worked"]]]
::error-resp [:string {:min 1}]})]
(is (= {:definitions {"malli.swagger-test.error-resp" {:minLength 1, :type "string"}
"malli.swagger-test.req-body" {:additionalProperties {}, :type "object"}
"malli.swagger-test.success-resp" {:properties {:it {:const "worked"}}
:required [:it]
:type "object"}}
:parameters [{:description ""
:in "body"
:name "body"
:required true
:schema {:$ref "#/definitions/malli.swagger-test.req-body"}}
{:description ""
:in "query"
:name :a
:required true
:type "integer"
:format "int64"}
{:description ""
:in "query"
:name :b
:required true
:type "string"
:minLength 10}]
:responses {200 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.success-resp"}}
400 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.error-resp"}}}}
(swagger/swagger-spec {::swagger/parameters
{:body (m/schema ::req-body
{:registry registry})
:query (m/schema ::query
{:registry registry})}
::swagger/responses
{200 {:schema (m/schema ::success-resp
{:registry registry})}
400 {:schema (m/schema ::error-resp
{:registry registry})}}})))))
(testing "no schema in responses ignored"
(is (= {:responses {200 {:description "" :schema {:type "string"}}
500 {:description "fail"}}}
(swagger/swagger-spec {::swagger/responses
{500 {:description "fail"}
200 {:schema [:string]}}}))))
(testing "generates swagger for ::parameters and ::responses w/ recursive schema + registry"
(let [registry (merge (m/base-schemas) (m/type-schemas)
(m/comparator-schemas) (m/sequence-schemas)
{::a [:or
:string
[:vector [:ref ::b]]]
::b [:or
:keyword
[:vector [:ref ::c]]]
::c [:or
:symbol
[:vector [:ref ::a]]]
::req-body [:map [:a ::a]]
::success-resp [:map-of :keyword :string]
::error-resp :string})]
(testing "not an infinite schema"
(is (not (m/validate (m/schema [:ref ::a] {:registry registry}) nil)))
(is (not (m/validate (m/schema [:ref ::b] {:registry registry}) nil)))
(is (not (m/validate (m/schema [:ref ::c] {:registry registry}) nil))))
(is (= {:definitions {"malli.swagger-test.a" {:type "string"
:x-anyOf [{:type "string"}
{:type "array"
:items {:$ref "#/definitions/malli.swagger-test.b"}}]}
"malli.swagger-test.b" {:type "string"
:x-anyOf [{:type "string"}
{:type "array"
:items {:$ref "#/definitions/malli.swagger-test.c"}}]}
"malli.swagger-test.c" {:type "string"
:x-anyOf [{:type "string"}
{:type "array"
:items {:$ref "#/definitions/malli.swagger-test.a"}}]}
"malli.swagger-test.error-resp" {:type "string"}
"malli.swagger-test.req-body" {:properties {:a {:$ref "#/definitions/malli.swagger-test.a"}}
:required [:a]
:type "object"}
"malli.swagger-test.success-resp" {:additionalProperties {:type "string"}
:type "object"}}
:parameters [{:description ""
:in "body"
:name "body"
:required true
:schema {:$ref "#/definitions/malli.swagger-test.req-body"}}]
:responses {200 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.success-resp"}}
400 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.error-resp"}}}}
(swagger/swagger-spec {::swagger/parameters
{:body (m/schema ::req-body
{:registry registry})}
::swagger/responses
{200 {:schema (m/schema ::success-resp
{:registry registry})}
400 {:schema (m/schema ::error-resp
{:registry registry})}}})))))
(testing "generates swagger for ::parameters and ::responses w/ var schema"
(is (= {:definitions {"malli.swagger-test.Request" {:additionalProperties {}, :type "object"},
"malli.swagger-test.Success" {:properties {:it {:$ref "#/definitions/malli.swagger-test.SuccessWorked"}},
:required [:it]
:type "object"}
"malli.swagger-test.SuccessWorked" {:const "worked"}}
:parameters [{:description ""
:in "body"
:name "body"
:required true
:schema {:$ref "#/definitions/malli.swagger-test.Request"}}]
:responses {200 {:description ""
:schema {:$ref "#/definitions/malli.swagger-test.Success"}}}}
(swagger/swagger-spec {::swagger/parameters {:body #'Request}
::swagger/responses {200 {:schema #'Success}}}))))
(testing "::parameters :query w/ var schema"
;; NB! all refs get inlined!
(is (= {:parameters [{:description ""
:in "query"
:name :a
:required true
:format "int64"
:type "integer"}
{:description ""
:in "query"
:name :b
:required true
:type "string"
:minLength 10}]}
(swagger/swagger-spec {::swagger/parameters {:query #'Query}})))))
(deftest request-parameter-definition-regression-test
;; For issue #1002
(testing "collects :definitions for all parameters"
(let [registry (merge (m/base-schemas) (m/type-schemas) (m/comparator-schemas)
{::req-body [:map-of :keyword :any]})
expected {:definitions {"malli.swagger-test.req-body" {:additionalProperties {}, :type "object"}}
:parameters [{:description ""
:in "body"
:name "body"
:required true
:schema {:$ref "#/definitions/malli.swagger-test.req-body"}}
{:description ""
:in "header"
:name :h
:required true
:type "string"}
{:description ""
:in "query"
:name :q
:required true
:type "string"}]}
fix #(update % :parameters (partial sort-by :in))]
(is (= expected
(fix
(swagger/swagger-spec {::swagger/parameters
{:body (m/schema ::req-body {:registry registry})
:header [:map [:h :string]]
:query [:map [:q :string]]}}))))
;; bug #1002 was sensitive to the order of the ::swagger/parameters map
(is (= expected
(fix
(swagger/swagger-spec {::swagger/parameters
{:header [:map [:h :string]]
:query [:map [:q :string]]
:body (m/schema ::req-body {:registry registry})}})))))))
+5
View File
@@ -0,0 +1,5 @@
(ns malli.test-macros)
(defmacro when-env [s & body]
(when (System/getenv s)
`(do ~@body)))
File diff suppressed because it is too large Load Diff
+1146
View File
File diff suppressed because it is too large Load Diff