init research
This commit is contained in:
Vendored
+42
@@ -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))))
|
||||
Vendored
+36
@@ -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]}}))
|
||||
|
||||
Vendored
+29
@@ -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}))))
|
||||
Vendored
+166
@@ -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))))))
|
||||
Vendored
+3699
File diff suppressed because it is too large
Load Diff
Vendored
+27
@@ -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
@@ -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)))))
|
||||
Vendored
+32
@@ -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
@@ -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)))))))
|
||||
Vendored
+8
@@ -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)))))
|
||||
|
||||
Vendored
+25
@@ -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)))))
|
||||
Vendored
+28
@@ -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
@@ -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)))))
|
||||
Vendored
+85
@@ -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]]))))
|
||||
Vendored
+945
@@ -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
@@ -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
@@ -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
@@ -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)))))))
|
||||
@@ -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}))))
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)))))))
|
||||
Vendored
+75
@@ -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
@@ -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
@@ -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"))
|
||||
Vendored
+1142
File diff suppressed because it is too large
Load Diff
+216
@@ -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
@@ -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
@@ -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)
|
||||
Vendored
+161
@@ -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
@@ -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
@@ -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})}))))
|
||||
Vendored
+311
@@ -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)))))
|
||||
Vendored
+37
@@ -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]]))))
|
||||
Vendored
+164
@@ -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))))))
|
||||
Vendored
+133
@@ -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!)))))))))
|
||||
Vendored
+567
@@ -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})}})))))))
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
(ns malli.test-macros)
|
||||
|
||||
(defmacro when-env [s & body]
|
||||
(when (System/getenv s)
|
||||
`(do ~@body)))
|
||||
Vendored
+1231
File diff suppressed because it is too large
Load Diff
Vendored
+1146
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user