(ns exoscale.coax-test #?(:cljs (:require-macros [cljs.test :refer [deftest testing is are run-tests]] [exoscale.coax :as sc])) (:require #?(:clj [clojure.test :refer [deftest testing is are]]) [clojure.spec.alpha :as s] [clojure.string :as str] [clojure.test.check :as tc] [clojure.test.check.generators] [clojure.test.check.properties :as prop] [clojure.spec.test.alpha :as st] #?(:clj [clojure.test.check.clojure-test :refer [defspec]]) #?(:cljs [clojure.test.check.clojure-test :refer-macros [defspec]]) [exoscale.coax :as sc] [exoscale.coax.coercer :as c]) #?(:clj (:import (java.net URI)))) #?(:clj (st/instrument)) (s/def ::infer-int int?) (s/def ::infer-and-spec (s/and int? #(> % 10))) (s/def ::infer-and-spec-indirect (s/and ::infer-int #(> % 10))) (s/def ::infer-form (s/coll-of int?)) (s/def ::infer-nilable (s/nilable int?)) #?(:clj (s/def ::infer-decimal? decimal?)) (sc/def ::some-coercion c/to-long) (s/def ::first-layer int?) (sc/def ::first-layer (fn [x _] (inc (c/to-long x nil)))) (s/def ::second-layer ::first-layer) (s/def ::second-layer-and (s/and ::first-layer #(> % 10))) (s/def ::or-example (s/or :int int? :double double? :bool boolean?)) (s/def ::nilable-int (s/nilable ::infer-int)) (s/def ::nilable-pos-int (s/nilable (s/and ::infer-int pos?))) (s/def ::nilable-string (s/nilable string?)) (s/def ::nilable-set #{nil}) (s/def ::int-set #{1 2}) (s/def ::float-set #{1.2 2.1}) (s/def ::boolean-set #{true}) (s/def ::symbol-set #{'foo/bar 'bar/foo}) (s/def ::ident-set #{'foo/bar :bar/foo}) (s/def ::string-set #{"hey" "there"}) (s/def ::keyword-set #{:a :b}) (s/def ::uuid-set #{#uuid "d6e73cc5-95bc-496a-951c-87f11af0d839" #uuid "a6e73cc5-95bc-496a-951c-87f11af0d839"}) (s/def ::nil-set #{nil}) #?(:clj (s/def ::uri-set #{(URI. "http://site.com") (URI. "http://site.org")})) #?(:clj (s/def ::decimal-set #{42.42M 1.1M})) (def enum-set #{:a :b}) (s/def ::referenced-set enum-set) (def enum-map {:foo "bar" :baz "qux"}) (s/def ::calculated-set (->> enum-map keys (into #{}))) (s/def ::nilable-referenced-set (s/nilable enum-set)) (s/def ::nilable-calculated-set (s/nilable (->> enum-map keys (into #{})))) (s/def ::nilable-referenced-set-kw (s/nilable ::referenced-set)) (s/def ::nilable-calculated-set-kw (s/nilable ::calculated-set)) (s/def ::unevaluatable-spec (letfn [(pred [x] (string? x))] (s/spec pred))) (sc/def ::some-coercion c/to-long) (deftest test-coerce-from-registry (testing "it uses the registry to coerce a key" (is (= (sc/coerce ::some-coercion "123") 123))) (testing "it returns original value when it a coercion can't be found" (is (= (sc/coerce ::not-defined "123") "123"))) (testing "go over nilables" (is (= (sc/coerce ::infer-nilable "123") 123)) (is (= (sc/coerce ::infer-nilable nil) nil)) (is (= (sc/coerce ::infer-nilable "") "")) (is (= (sc/coerce ::nilable-int "10") 10)) (is (= (sc/coerce ::nilable-int "10" {::sc/idents {`int? (fn [x _] (keyword x))}}) :10)) (is (= (sc/coerce ::nilable-pos-int "10") 10)) (is (= (sc/coerce ::nilable-string nil) nil)) (is (= (sc/coerce ::nilable-string 1) "1")) (is (= (sc/coerce ::nilable-string "") "")) (is (= (sc/coerce ::nilable-string "asdf") "asdf"))) (testing "specs given as sets" (is (= (sc/coerce ::nilable-set nil) nil)) (is (= (sc/coerce ::int-set "1") 1)) (is (= (sc/coerce ::float-set "1.2") 1.2)) (is (= (sc/coerce ::boolean-set "true") true)) ;;(is (= (sc/coerce ::symbol-set "foo/bar") 'foo/bar)) (is (= (sc/coerce ::string-set "hey") "hey")) (is (= (sc/coerce ::keyword-set ":b") :b)) (is (= (sc/coerce ::uuid-set "d6e73cc5-95bc-496a-951c-87f11af0d839") #uuid "d6e73cc5-95bc-496a-951c-87f11af0d839")) ;;#?(:clj (is (= (sc/coerce ::uri-set "http://site.com") (URI. "http://site.com")))) #?(:clj (is (= (sc/coerce ::decimal-set "42.42M") 42.42M))) ;; The following tests can't work without using `eval`. We will avoid this ;; and hope that spec2 will give us a better way. ;;(is (= (sc/coerce ::referenced-set ":a") :a)) ;;(is (= (sc/coerce ::calculated-set ":foo") :foo)) ;;(is (= (sc/coerce ::nilable-referenced-set ":a") :a)) ;;(is (= (sc/coerce ::nilable-calculated-set ":foo") :foo)) ;;(is (= (sc/coerce ::nilable-referenced-set-kw ":a") :a)) ;;(is (= (sc/coerce ::nilable-calculated-set-kw ":foo") :foo)) (is (= (sc/coerce ::unevaluatable-spec "just a string") "just a string")))) (deftest test-coerce! (is (= (sc/coerce! ::infer-int "123") 123)) (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo :cljs js/Error) #"Invalid coerced value" (sc/coerce! ::infer-int "abc")))) (deftest test-conform (is (= (sc/conform ::or-example "true") [:bool true]))) (deftest test-coerce-from-predicates (are [predicate input output] (= (sc/coerce predicate input) output) `number? "42" 42 `number? "foo" "foo" `integer? "42" 42 `int? "42" 42 `int? 42.0 42 `int? 42.5 42 `(s/int-in 0 100) "42" 42 `pos-int? "42" 42 `neg-int? "-42" -42 `nat-int? "10" 10 `even? "10" 10 `odd? "9" 9 `float? "42.42" 42.42 `double? "42.42" 42.42 `double? 42.42 42.42 `double? 42 42.0 `number? "42.42" 42.42 `number? 42.42 42.42 `number? 42 42 `(s/double-in 0 100) "42.42" 42.42 `string? 42 "42" `string? :a ":a" `string? :foo/bar ":foo/bar" `string? [] [] `string? {} {} `string? #{} #{} `boolean? "true" true `boolean? "false" false `ident? ":foo/bar" :foo/bar `ident? "foo/bar" 'foo/bar `simple-ident? ":foo" :foo `qualified-ident? ":foo/baz" :foo/baz `keyword? "keyword" :keyword `keyword? ":keyword" :keyword `keyword? 'symbol :symbol `simple-keyword? ":simple-keyword" :simple-keyword `qualified-keyword? ":qualified/keyword" :qualified/keyword `symbol? "sym" 'sym `simple-symbol? "simple-sym" 'simple-sym `qualified-symbol? "qualified/sym" 'qualified/sym `uuid? "d6e73cc5-95bc-496a-951c-87f11af0d839" #uuid "d6e73cc5-95bc-496a-951c-87f11af0d839" `nil? nil nil `false? "false" false `true? "true" true `zero? "0" 0 `(s/coll-of int?) ["11" "31" "42"] [11 31 42] `(s/coll-of int?) ["11" "foo" "42"] [11 "foo" 42] `(s/coll-of int? :kind list?) ["11" "foo" "42"] '(11 "foo" 42) `(s/coll-of int? :kind set?) ["11" "foo" "42"] #{11 "foo" 42} `(s/coll-of int? :kind set?) #{"11" "foo" "42"} #{11 "foo" 42} `(s/coll-of int? :kind vector?) '("11" "foo" "42") [11 "foo" 42] `(s/every int?) ["11" "31" "42"] [11 31 42] `(s/map-of keyword? int?) {"foo" "42" "bar" "31"} {:foo 42 :bar 31} `(s/map-of keyword? int?) "foo" "foo" `(s/every-kv keyword? int?) {"foo" "42" "bar" "31"} {:foo 42 :bar 31} `(s/or :int int? :double double? :bool boolean?) "42" 42 `(s/or :double double? :bool boolean?) "42.3" 42.3 `(s/or :int int? :double double? :bool boolean?) "true" true `(s/or :b keyword? :a string?) "abc" "abc" `(s/or :a string? :b keyword?) "abc" "abc" `(s/or :b keyword? :a string?) :abc :abc `(s/or :str string? :kw keyword? :number? number?) :asdf :asdf `(s/or :str string? :kw keyword? :number? number?) "asdf" "asdf" `(s/or :kw keyword? :str string? :number? number?) "asdf" "asdf" `(s/or :number? number? :kw keyword?) "1" 1 `(s/or :number? number?) "1" 1 `(s/or :number? number? :kw keyword? :str string?) "1" "1" `(s/or :number? number? :kw keyword? :str string?) 1 1 #{:a :b} "a" :a #{1 2} "1" 1 #?@(:clj [`uri? "http://site.com" (URI. "http://site.com")]) #?@(:clj [`decimal? "42.42" 42.42M `decimal? "42.42M" 42.42M]))) (def test-gens {`inst? (s/gen (s/inst-in #inst "1980" #inst "9999"))}) #?(:cljs (defn ->js [var-name] (-> (str var-name) (str/replace #"/" ".") (str/replace #"-" "_") (str/replace #"\?" "_QMARK_") (js/eval)))) (defn safe-gen [s sp] (try (or (test-gens s) (s/gen sp)) (catch #?(:clj Exception :cljs :default) _ nil))) #?(:clj ;; FIXME won't run on cljs (deftest test-coerce-generative (doseq [s (->> (sc/registry) ::sc/idents (keys) (filter symbol?)) :let [sp #?(:clj @(resolve s) :cljs (->js s)) gen (safe-gen s sp)] :when gen] (let [res (tc/quick-check 100 (prop/for-all [v gen] (s/valid? sp (sc/coerce s (-> (pr-str v) (str/replace #"^#[^\"]+\"|\"]?$" ""))))))] (if-not (= true (:result res)) (throw (ex-info (str "Error coercing " s) {:symbol s :spec sp :result res}))))))) #?(:clj (deftest test-coerce-inst (are [input output] (= (sc/coerce `inst? input) output) "2020-05-17T21:37:57.830-00:00" #inst "2020-05-17T21:37:57.830-00:00" "2018-09-28" #inst "2018-09-28"))) (deftest test-coerce-inference-test (are [keyword input output] (= (sc/coerce keyword input) output) ::infer-int "123" 123 ::infer-and-spec "42" 42 ::infer-and-spec-indirect "43" 43 ::infer-form ["20" "43"] [20 43] ::infer-form '("20" "43") '(20 43) ::infer-form (map str (range 2)) '(0 1) ::second-layer "41" 42 ::second-layer-and "41" 42 #?@(:clj [::infer-decimal? "123.4" 123.4M]) #?@(:clj [::infer-decimal? 123.4 123.4M]) #?@(:clj [::infer-decimal? 123.4M 123.4M]) #?@(:clj [::infer-decimal? "" ""]) #?@(:clj [::infer-decimal? [] []]))) (deftest test-coerce-structure (is (= (sc/coerce-structure {::some-coercion "321" ::not-defined "bla" :sub {::infer-int "42"}}) {::some-coercion 321 ::not-defined "bla" :sub {::infer-int 42}})) (is (= (sc/coerce-structure {::some-coercion "321" ::not-defined "bla" :unqualified 12 :sub {::infer-int "42"}} {::sc/idents {::not-defined `keyword?}}) {::some-coercion 321 ::not-defined :bla :unqualified 12 :sub {::infer-int 42}})) (is (= (sc/coerce-structure {::or-example "321"} {::sc/op sc/conform}) {::or-example [:int 321]}))) (s/def ::bool boolean?) (s/def ::simple-keys (s/keys :req [::infer-int] :opt [::bool])) (s/def ::nested-keys (s/keys :req [::infer-form ::simple-keys] :req-un [::bool])) (deftest test-coerce-keys (is (= {::infer-int 123} (sc/coerce ::simple-keys {::infer-int "123"}))) (is (= {::infer-form [1 2 3] ::simple-keys {::infer-int 456 ::bool true} :bool true} (sc/coerce ::nested-keys {::infer-form ["1" "2" "3"] ::simple-keys {::infer-int "456" ::bool "true"} :bool "true"}))) (is (= "garbage" (sc/coerce ::simple-keys "garbage")))) (s/def ::head double?) (s/def ::body int?) (s/def ::arm int?) (s/def ::leg double?) (s/def ::arms (s/coll-of ::arm)) (s/def ::legs (s/coll-of ::leg)) (s/def ::name string?) (s/def ::animal (s/keys :req [::head ::body ::arms ::legs] :opt-un [::name ::id])) (deftest test-coerce-with-registry-overrides (testing "it uses overrides when provided" (is (= {::head 1 ::body 16 ::arms [4 4] ::legs [7 7] :foo "bar" :name :john} (sc/coerce ::animal {::head "1" ::body "16" ::arms ["4" "4"] ::legs ["7" "7"] :foo "bar" :name "john"} {::sc/idents {::head c/to-long ::leg c/to-long ::name c/to-keyword}})) "Coerce with option form") (is (= 1 (sc/coerce `string? "1" {::sc/idents {`string? c/to-long}})) "overrides works on qualified-idents") (is (= [1] (sc/coerce `(s/coll-of string?) ["1"] {::sc/idents {`string? c/to-long}})) "overrides works on qualified-idents, also with composites") (is (= ["foo" "bar" "baz"] (sc/coerce `vector? "foo,bar,baz" {::sc/idents {`vector? (fn [x _] (str/split x #"[,]"))}})) "override on real world use case with vector?"))) (s/def ::foo int?) (s/def ::bar string?) (s/def ::qualified (s/keys :req [(or ::foo ::bar)])) (s/def ::unqualified (s/keys :req-un [(or ::foo ::bar)])) (deftest test-or-conditions-in-qualified-keys (is (= (sc/coerce ::qualified {::foo "1" ::bar "hi"}) {::foo 1 ::bar "hi"}))) (deftest test-or-conditions-in-unqualified-keys (is (= (sc/coerce ::unqualified {:foo "1" :bar "hi"}) {:foo 1 :bar "hi"}))) (s/def ::tuple (s/tuple ::foo ::bar int?)) (deftest test-tuple (is (= [0 "" 1] (sc/coerce ::tuple ["0" nil "1"]))) (is (= "garbage" (sc/coerce ::tuple "garbage")))) (deftest test-merge (s/def ::merge (s/merge (s/keys :req-un [::foo]) ::unqualified ;; TODO: add s/multi-spec test )) (is (= {:foo 1 :bar "1" :c {:a 2}} (sc/coerce ::merge {:foo "1" :bar 1 :c {:a 2}})) "Coerce new vals appropriately") (is (= {:foo 1 :bar "1" :c {:a 2}} (sc/coerce ::merge {:foo 1 :bar "1" :c {:a 2}})) "Leave out ok vals") (s/def ::merge2 (s/merge (s/keys :req [::foo]) ::unqualified)) (is (= {::foo 1 :bar "1" :c {:a 2} :foo 1} (sc/coerce ::merge2 {::foo "1" :foo "1" :bar "1" :c {:a 2}})) "Leave out ok vals") (is (= "garbage" (sc/coerce ::merge "garbage")) "garbage is passthrough") (s/def ::x qualified-keyword?) (sc/def ::x (fn [x _] (keyword "y" x))) (s/def ::m1 (s/keys :opt [::x])) (s/def ::mm (s/merge ::m1 ::m1)) (is (= {::x :y/quux} (sc/coerce ::mm {::x "quux"} {::sc/cache? false})))) (def d :kw) ;; no vars in cljs #?(:clj (defmulti multi #'d) :cljs (defmulti multi :kw)) (defmethod multi :default [_] (s/keys :req-un [::foo])) (defmethod multi :kw [_] ::unqualified) (s/def ::multi (s/multi-spec multi :hit)) (deftest test-multi-spec (is (= {:not "foo"} (sc/coerce ::multi {:not "foo"}))) (is (= {:foo 1} (sc/coerce ::multi {:foo 1}))) (is (= {:foo 1} (sc/coerce ::multi {:foo "1"}))) (is (= {:foo 1 :d :kw} (sc/coerce ::multi {:d :kw :foo "1"}))) (is (= "garbage" (sc/coerce ::multi "garbage")))) (deftest test-gigo (is (= (sc/coerce `(some-unknown-form string?) 1) 1)) (is (= (sc/coerce `(some-unknown-form) 1) 1))) (deftest invalidity-test (is (= :exoscale.coax/invalid (sc/coerce* `int? [] {}))) (is (= :exoscale.coax/invalid (sc/coerce* `(s/coll-of int?) 1 {}))) (is (= :exoscale.coax/invalid (sc/coerce* ::int-set "" {})))) (deftest test-caching (s/def ::bs (s/keys :req [::bool])) (is (= false (sc/coerce ::bool "false"))) (is (= false (::bool (sc/coerce ::bs {::bool "false"})))) (is (= false (sc/coerce ::bool "false" {:exoscale.coax/cache? false}))) (is (= false (::bool (sc/coerce ::bs {::bool "false"} {:exoscale.coax/cache? false})))))