bouncer

0.3.3


A validation DSL for Clojure apps

dependencies

org.clojure/clojure
1.6.0
clj-time
0.9.0
com.andrewmcveigh/cljs-time
0.3.0



(this space intentionally left almost blank)
 

0.3.3

(defproject bouncer 
  :description "A validation DSL for Clojure apps"
  :url "http://github.com/leonardoborges/bouncer"
  :license {:name "MIT License"
            :url "http://opensource.org/licenses/MIT"}
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [clj-time "0.9.0"]
                 [com.andrewmcveigh/cljs-time "0.3.0"]]
  :jar-exclusions [#"\.cljx"]
  :source-paths ["src" "target/classes"]
  :test-paths ["target/test-classes"]
  :prep-tasks [["cljx-once"]]
  :profiles {:1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}
             :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}
             :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}
             :cljs {:dependencies [[org.clojure/clojurescript "0.0-2665"]]
                    :plugins [[lein-cljsbuild "1.0.3"]
                              [com.cemerick/clojurescript.test ]]
                    :cljsbuild {:test-commands {"phantom" ["phantomjs" :runner "target/testable.js"]}
                                :builds [{:source-paths ["target/classes" "target/test-classes"]
                                         :compiler {:output-to "target/testable.js"
                                                    :optimizations :whitespace}}]}
                    :prep-tasks [["cljsbuild" "once"]]
                    :hooks [leiningen.cljsbuild]}
             :cljx {:plugins [[com.keminglabs/cljx "0.6.0"]]
                    :cljx {:builds [{:source-paths ["src"]
                                     :output-path "target/classes"
                                     :rules :clj}
                                    {:source-paths ["src"]
                                     :output-path "target/classes"
                                     :rules :cljs}
                                    {:source-paths ["test"]
                                     :output-path "target/test-classes"
                                     :rules :clj}
                                    {:source-paths ["test"]
                                     :output-path "target/test-classes"
                                     :rules :cljs}]}}}
  :aliases {"all-tests" ["with-profile" "cljs:1.4:1.5:1.6" "test"]
            "cljx-auto" ["with-profile" "cljx" "cljx" "auto"]
            "cljx-once" ["with-profile" "cljx" "cljx" "once"]})
 

The core namespace provides the two main entry point functions in bouncer:

  • validate
  • valid?

All other functions are meant for internal use only and shouldn't be relied upon.

The project README should get you started, it's pretty comprehensive.

If you'd like to know more about the motivation behind bouncer, check the announcement post.

(ns bouncer.core
  {:author "Leonardo Borges"})

Internal utility functions

(defn- build-multi-step
  ([key-or-vec fn-vec] (build-multi-step key-or-vec fn-vec []))
  ([key-or-vec [f-or-list & rest] acc]
     (if-not f-or-list
       acc
       (cond
        (sequential? f-or-list)
        (let [[f & opts] f-or-list]
          (recur key-or-vec
                 rest
                 (conj acc (concat [f key-or-vec ] opts))))
        :else (recur key-or-vec
                     rest
                     (conj acc [f-or-list key-or-vec]))))))

Takes two arguments:

parent-keyword is a :keyword - or a vector of :keywords denoting a path in a associative structure

validations-map is a map of forms following this spec:

  {:keyword [f g] :another-keyword h}

Merges :parent-keyword with every first element of validations-map, transforming it into:

  ([:parent-keyword :keyword] [f g] [:parent-keyword :another-keyword] h)
(defn- merge-path
  [parent-key validations-map]
  (let [parent-key (if (keyword? parent-key) [parent-key] parent-key)]
    (mapcat (fn [[key validations]]
              (if (vector? key)
                [(apply vector (concat parent-key key)) validations]
                [(apply vector (concat parent-key [key])) validations]))
            validations-map)))
(defn- build-steps [[head & tail :as forms]]
  (let [forms (if (map? head)
                (vec (mapcat identity head))
                forms)]
    (reduce (fn [acc [key-or-vec sym-or-coll :as rule]]
              (cond
               (vector? sym-or-coll)
               (concat acc (build-multi-step key-or-vec sym-or-coll))
               (map? sym-or-coll)
               (concat acc (build-steps (merge-path key-or-vec
                                                    sym-or-coll)))
               :else (conj acc [sym-or-coll key-or-vec])))
            []
            (partition 2 forms))))
(defn- pre-condition-met? [pre-fn map]
  (or (nil? pre-fn) (pre-fn map)))

Wraps pred in the context of validating a single value

  • acc is the map being validated

  • pred is a validator

  • k the path to the value to be validated in the associative structure acc

  • args any extra args to pred

    It only runs pred if:

  • the validator contains a pre-condition and it is met or;

  • the validator is optional and there is a non-nil value to be validated (this information is read from pred's metadata) or;
  • there are no previous errors for the given path

    Returns acc augmented with a namespace qualified ::errors keyword

(defn- wrap
  [message-fn acc [pred k & args]]
  (let [k (if (vector? k) k [k])
        error-path (cons ::errors k)
        {:keys [optional default-message-format]
         :or {optional false
              default-message-format "Custom validation failed for %s"}
         :as metadata} (meta pred)
        meta-with-defaults
        (merge metadata {:default-message-format default-message-format
                         :optional optional})
        [args opts] (split-with (complement keyword?) args)
        {:keys [message pre]} (apply hash-map opts)
        pred-subject (get-in acc k)]
    (if (pre-condition-met? pre acc)
      (if (or (and optional (nil? pred-subject))
              (not (empty? (get-in acc error-path)))
              (apply pred pred-subject args))
        acc
        (update-in acc error-path
                   #(conj % (message-fn {:path k, :value pred-subject
                                         :args (seq args)
                                         :metadata meta-with-defaults
                                         :message message}))))
      acc)))

Internal Use.

Chain of responsibility.

Takes the current state and a collection of validators fs

Will run all validators against old-state and eventually return a vector with the result - the errors map - and the new state - the original map augmented with the errors map.

See also wrap

(defn- wrap-chain
  [old-state message-fn & fs]
  (let [new-state (reduce (partial wrap message-fn)
                          old-state
                          fs)]
    [(::errors new-state) new-state]))

Internal use.

Validates the map m using the validation functions fs.

Returns a vector where the first element is the map of validation errors if any and the second is the original map (possibly) augmented with the errors map.

(defn- validate*
  [message-fn m fs]
  (loop [[errors state :as ret] [nil m]
         fs fs]
    (if (seq fs)
      (recur (wrap-chain state message-fn (first fs)) (rest fs))
      ret)))

Public API

Use together with validate, e.g.:

  (core/validate core/with-default-messages {}
                 :name v/required)
(defn with-default-messages
  [error]
  (let [{:keys [message path metadata]} error]
          (format (or message (:default-message-format metadata))
                  (name (peek path)))))

Takes a

  • message-fn (optional) responsible for transforming error metadata into the validation result (defaults to with-default-messages)

  • m map to be validated

  • forms validations to be performed on the map

    forms can be a single validator set or a sequence of key/value pairs where:

    key ==> :keyword or [:a :path]

    value ==> validation-function or validator-set or [[validation-function args+opts]] or [validation-function another-validation-function] or [validation-function [another-validation-function args+opts]]

    e.g.:

    (core/validate a-map :name v/required :age [v/required [v/number :message "age must be a number"]] [:passport :number] v/positive)

    Returns a vector where the first element is the map of validation errors if any and the second is the original map (possibly) augmented with the errors map.

    See also defvalidator

(defn validate
  [& args]
  (let [[message-fn args] (if (fn? (first args))
                                   [(first args) (next args)]
                                   [with-default-messages args])
        [m forms] [(first args) (next args)]]
    (validate* message-fn m (build-steps forms))))

Takes a map and one or more validation functions with semantics provided by "validate". Returns true if the map passes all validations. False otherwise.

(defn valid?
  [& args]
  (empty? (first (apply validate args))))

This file autogenerated from src/bouncer/core.cljx

 

This namespace contains all built-in validators as well as macros for defining new validators and validator sets

(ns bouncer.validators
  {:author "Leonardo Borges"}
        (:require [clj-time.format :as f])
  (:refer-clojure :exclude [boolean]))

Customization support

The following functions and macros support creating custom validators

Defines a new validating function using args & body semantics as provided by "defn". docstring and opts-map are optional

opts-map is a map of key-value pairs and may be one of:

  • :default-message-format used when the client of this validator doesn't provide a message (consider using custom message functions)

  • :optional whether the validation should be run only if the given key has a non-nil value in the map. Defaults to false.

    or any other key-value pair which will be available in the validation result under the :metadata key.

    The function will be called with the value being validated as its first argument.

    Any extra arguments will be passed along to the function in the order they were used in the "validate" call.

    e.g.:

    (defvalidator member [value coll] (some #{value} coll))

    (validate {:age 10} :age [[member (range 5)]])

    This means the validator member will be called with the arguments 10 and (0 1 2 3 4), in that order.

(defmacro defvalidator
  {:arglists '([name docstring? opts-map? [args*] & body])}
  [name & options]
  (let [[docstring options] (if (string? (first options))
                                 [(first options) (next options)]
                                 [nil options])
        [fn-meta [args & body]] (if (map? (first options))
                                  [(first options) (next options)]
                                  [nil options])
        fn-meta (assoc fn-meta
                       :validator (keyword (str *ns*) (str name)))]
    (let [arglists ''([name])]
      `(do (def ~name (with-meta (fn ~name
                                   ([~@args]
                                    ~@body)) ~fn-meta))))))

Built-in validators

Validates value is present.

If the value is a string, it makes sure it's not empty, otherwise it checks for nils.

For use with validation functions such as validate or valid?

(defvalidator required
  {:default-message-format "%s must be present"}
  [value]
  (if (string? value)
    (not (empty? value))
    (not (nil? value))))

Validates maybe-a-number is a valid number.

For use with validation functions such as validate or valid?

(defvalidator number
  {:default-message-format "%s must be a number" :optional true}
  [maybe-a-number]
  (number? maybe-a-number))

Validates maybe-an-int is a valid integer.

For use with validation functions such as validate or valid?

(defvalidator integer
  {:default-message-format "%s must be an integer"}
  [maybe-an-int]
  (integer? maybe-an-int))

Validates maybe-a-boolean is a valid boolean.

For use with validation functions such as validate or valid?

(defvalidator boolean
  {:default-message-format "%s must be a boolean"}
  [maybe-a-boolean]
  (or (= false maybe-a-boolean)
      (= true maybe-a-boolean)))

Validates maybe-a-string is a valid string.

For use with validation functions such as validate or valid?

(defvalidator string
  {:default-message-format "%s must be a string"}
  [maybe-a-string]
  (string? maybe-a-string))

Validates number is inside specified range [from to].

For use with validation functions such as validate or valid?

(defvalidator in-range
  {:default-message-format "%s must be in a specified range"}
  [value [from to]]
  (<= from value to))

Validates number is a number and is greater than zero.

For use with validation functions such as validate or valid?

(defvalidator positive
  {:default-message-format "%s must be a positive number" :optional true}
  [number]
  (> number 0))

Validates value is a member of coll.

For use with validation functions such as validate or valid?

(defvalidator member
  {:default-message-format "%s must be one of the values in the list"}
  [value coll]
  (some #{value} coll))

Validates pred is true for the given value.

For use with validation functions such as validate or valid?

(defvalidator custom
  [value pred]
  (println "Warning: bouncer.validators/custom is deprecated and will be removed. Use plain functions instead.")
  (pred value))

Validates pred is true for every item in coll.

For use with validation functions such as validate or valid?

(defvalidator every
  {:default-message-format "All items in %s must satisfy the predicate"}
  [coll pred]
  (every? pred coll))

Validates value satisfies the given regex pattern.

For use with validation functions such as validate or valid?

(defvalidator matches
  {:default-message-format "%s must satisfy the given pattern" :optional true}
  [value re]
  ((complement empty?) (re-seq re value)))

Validates value is an email address.

It implements a simple check to verify there's only a '@' and at least one point after the '@'

For use with validation functions such as validate or valid?

(defvalidator email
  {:default-message-format "%s must be a valid email address"}
  [value]
  (and (required value) (matches value #"^[^@]+@[^@\\.]+[\\.].+")))

Validates value is a date(time).

Optionally, takes a formatter argument which may be either an existing clj-time formatter, or a string representing a custom datetime formatter.

For use with validation functions such as validate or valid?

(defvalidator datetime
  {:default-message-format "%s must be a valid date"}
  [value & [opt & _]]
  (let [formatter (if (string? opt) (f/formatter opt) opt)]
    (try
      (if formatter (f/parse formatter value) (f/parse value))
            (catch IllegalArgumentException e false))))

Validates value is not greater than a max count

For use with validation functions such as validate or valid?

(defvalidator max-count
  {:default-message-format "%s is longer than the maximum"}
  [value maximum]
  (<= (count (seq value)) maximum))

Validates value at least meets the minimum count

For use with validation functions such as validate or valid?

(defvalidator min-count
  {:default-message-format "%s is less than the minimum"}
  [value minimum]
  (>= (count (seq value)) minimum))

This file autogenerated from src/bouncer/validators.cljx

 
(ns bouncer.core-test
  (:require [bouncer.core :as core]
                  [clojure.test :refer [is deftest testing are]]
                  [bouncer.validators :as v :refer [defvalidator]]))
(deftest validations
  (testing "Required validations"
    (is (not (core/valid? {}
                          :name v/required)))
    (is (not (core/valid? {:name }
                          :name v/required)))
    (is (not (core/valid? {:name nil}
                          :name v/required)))
    (is (core/valid? {:name "Leo"}
                     :name v/required)))
  (testing "Number validations"
    ;; map entries are optional by default...
    (is (core/valid? {}
                     :age v/number))
    ;;unless otherwise specified
    (is (not (core/valid? {}
                          :age [v/required v/number])))
    (is (not (core/valid? {:age "invalid"}
                          :age [v/number v/positive])))
    (is (core/valid? {:age nil}
                     :age v/number))
    (is (core/valid? {:age 10}
                     :age v/number)))
  (testing "Custom validations"
    (is (not (core/valid? {}
                          :name (complement nil?))))
    (is (core/valid? {:name "Leo"}
                     :name (complement nil?)))
    (letfn [(not-nil [v] ((complement nil?) v))]
      (is (not (core/valid? {}
                            :name not-nil)))
      (is (core/valid? {:name "Leo"}
                       :name not-nil)))))
(deftest standard-functions
  (testing "it can use plain clojure functions as validators"
    (are [valid? subject    validations] (= valid? (core/valid? subject validations))
         false   {:age 0}   {:age [[pos? :message "positive"]]}
         true    {:age  10} {:age [[pos? :message "positive"]]}
         false   {:age 0}   {:age pos?}
         true    {:age  10} {:age pos?})))
(def map-no-street {:address {:street nil :country "Brazil"}})
(def map-with-street (assoc-in map-no-street [:address :street]
                               "Rock 'n Roll Boulevard"))
(deftest nested-maps
  (testing "nested validations"
    (is (not (core/valid? map-no-street
                          :address v/required
                          [:address :street] v/required)))
    (is (not (core/valid? map-no-street
                          [:address :street] v/required)))
    (is (core/valid? map-with-street
                     :address v/required
                     [:address :street] v/required))
    (is (core/valid? map-with-street
                          [:address :street] v/required))
    (is (not (core/valid? {}
                          [:address :street] v/required))))
  (testing "optional nested validations"
    (is (core/valid? {:passport {:issue-year 2012}}
                     [:passport :issue-year] v/number))
    (is (core/valid? {:passport {:issue-year nil}}
                     [:passport :issue-year] v/number))
    (is (not (core/valid? {:passport {:issue-year nil}}
                          [:passport :issue-year] [v/required v/number])))))
(defn first-error-for [key validation-result]
  (-> validation-result (first) key (first)))
(deftest validation-messages
  (testing "default messages"
    (is (= {
            :dob '("Custom validation failed for dob")
            :year '("year must be a number")
            :name '("name must be present")
            :age '("age must be a positive number")
            }
           (first (core/validate {:age -1 :year }
                    :name v/required
                    :year v/number
                    :age v/positive
                    :dob (complement nil?))))))
  (testing "custom messages"
    (is (= {
            :age '("Idade deve ser maior que zero")
            :year '("Ano eh obrigatorio")
            :name '("Nome eh obrigatorio")
            :dob  '("Nao pode ser nulo")
            }
           (first (core/validate {:age -1 :year }
                    :name [[v/required :message "Nome eh obrigatorio"]]
                    :year [[v/required :message "Ano eh obrigatorio"]]
                    :age [[v/positive :message "Idade deve ser maior que zero"]]
                    :dob [[(complement nil?) :message "Nao pode ser nulo"]]))))))
(deftest validation-result
  (testing "invalid results"
    (let [[result map] (core/validate {:age -1 :year }
                                      :name v/required
                                      :year [v/required v/number]
                                      :age v/positive)]
      (is (= result (::core/errors map)))))
  (testing "valid results"
    (let [[result map] (core/validate {:name "Leo"}
                                      :name v/required)]
      (is (true? (and (empty? result)
                      (nil? (::core/errors map))))))))
(deftest coll-validations
  (let [valid-map   {:name "Leo" :pets [{:name "Aragorn"} {:name "Gandalf"}]}
        invalid-map {:name "Leo" :pets [{:name nil} {:name "Gandalf"}]}]
    (testing "nested colls"
      (is (core/valid? valid-map
                       :pets [[v/every #(not (nil? (:name %)))]]))
      (is (not (core/valid? invalid-map
                            :pets [[v/every #(not (nil? (:name %)))]]))))
    (testing "default messages for nested colls"
      (let [[result map] (core/validate invalid-map
                           :pets [[v/every #(not (nil? (:name %)))]])]
        (is (= "All items in pets must satisfy the predicate"
               (-> result :pets (first))))))
    (testing "custom messages for nested colls"
      (let [[result map] (core/validate invalid-map
                           :pets [[v/every #(not (nil? (:name %)))
                                   :message "All pets must have names"]])]
        (is (= "All pets must have names"
               (-> result :pets (first)))))))
  (testing "deep nested coll"
    (is (core/valid? {:name "Leo"
                      :address {:current { :country "Australia"}
                                :past [{:country "Spain"} {:country "Brasil"}]}}
                     [:address :past] [[v/every #(not (nil? (:country %)))]]))
    (is (not (core/valid? {:name "Leo"
                           :address {:current { :country "Australia"}
                                     :past [{:country "Spain"} {:country nil}]}}
                          [:address :past] [[v/every #(not (nil? (:country %)))]])))))
(deftest early-exit
  (testing "short circuit validations for single entry"
    (is (= {:age '("age must be present")}
           (first (core/validate {}
                    :age [v/required v/number v/positive]))))
    (is (= {:age '("age must be present")}
           (first (core/validate {:age }
                    :age [v/required v/number v/positive]))))
    (is (= {:age '("age must be a number")}
           (first (core/validate {:age "NaN"}
                    :age [v/required v/number v/positive]))))
    (is (= {:age '("age must be a positive number")}
           (first (core/validate {:age -7}
                    :age [v/required v/number v/positive]))))))
(defn pred-fn [x]
  (not (nil? (:country x))))
(deftest all-validations
  (testing "all built-in validators"
    (let [errors-map {
                      :age    '({:path [:age], :value , :args nil, :message nil
                                 :metadata {:default-message-format "%s must be present"
                                            :optional false
                                            :validator :bouncer.validators/required}})
                      :mobile '({:path [:mobile], :value nil, :args nil, :message "wrong format"
                                 :metadata {:default-message-format "Custom validation failed for %s"
                                            :optional false}})
                      :car    '({:path [:car], :value nil, :args [["Ferrari" "Mustang" "Mini"]], :message nil
                                 :metadata {:default-message-format "%s must be one of the values in the list"
                                            :optional false
                                            :validator :bouncer.validators/member}})
                      :dob    '({:path [:dob], :value "NaN", :args nil, :message nil
                                 :metadata {:default-message-format "%s must be a number"
                                            :optional true
                                            :validator :bouncer.validators/number}})
                      :name   '({:path [:name], :value nil, :args nil, :message nil
                                 :metadata {:default-message-format "%s must be present"
                                            :optional false
                                            :validator :bouncer.validators/required}})
                      :passport {:number '({:path [:passport :number], :value -7, :args nil, :message nil
                                            :metadata {:default-message-format "%s must be a positive number"
                                                       :optional true
                                                       :validator :bouncer.validators/positive}})}
                      :address  {:past   (list {:path [:address :past], :value [{:country nil} {:country "Brasil"}],
                                                :args [pred-fn] :message nil
                                                :metadata {:default-message-format "All items in %s must satisfy the predicate"
                                                           :optional false
                                                           :validator :bouncer.validators/every}})}
                      }
          invalid-map {:name nil
                       :age 
                       :passport {:number -7 :issued_by "Australia"}
                       :dob "NaN"
                       :address {:current { :country "Australia"}
                                 :past [{:country nil} {:country "Brasil"}]}}]
      (is (= errors-map
             (first (core/validate identity
                                   invalid-map
                                   :name v/required
                                   :age v/required
                                   :mobile [[string? :message "wrong format"]]
                                   :car [[v/member ["Ferrari" "Mustang" "Mini"]]]
                                   :dob v/number
                                   [:passport :number] v/positive
                                   [:address :past] [[v/every pred-fn]])))))))
(deftest pipelining-validations
  (testing "should preserve the existing errors map if there is one"
    (let [validation-errors (-> {:age "NaN"}
                            (core/validate :name v/required)
                            second
                            (core/validate :age v/number)
                            second
                            ::core/errors)]
      (is (= 2
             (count (select-keys validation-errors [:age :name])))))))
(deftest preconditions
  (testing "runs the current validation only if the pre-condition is met"
    (is (core/valid? {:a 1 :b "Z"}
                     :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]]))
    (is (not (core/valid? {:a 1 :b "X"}
                          :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]])))
    (is (core/valid? {:a -1 :b "Z"}
                     :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]]))
    (is (core/valid? {:a -1 :b "X"}
                     :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]]))
    (is (not (core/valid? {:a 1 :b "Z"}
                          :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]]
                          :c v/required)))))

This file autogenerated from test/bouncer/core_test.cljx

 
(ns bouncer.validators-test
  (:require [bouncer.core :as core]
            [bouncer.validators :as v]
                  [clojure.test :refer [deftest testing is]]
                  [clj-time.format :as f]))
(def addr-validator-set
  {:postcode [v/required v/number]
   :street    v/required
   :country   v/required
   :past      [[v/every #(not (nil? (:country %)))]]})
(def addr-validator-set+custom-messages
  {:postcode [[v/required :message "required"] [v/number :message "number"]]
   :street    [[v/required :message "required"]]
   :country   [[v/required :message "required"]]
   :past      [[v/every #(not (nil? (:country %))) :message "every"]]})
(def address-validator
  {:postcode v/required})
(def person-validator
  {:name [v/required v/string]
   :age [v/required v/number v/integer]
   :address address-validator})
(def deep-validator
  {:winner person-validator})
(def default-validate (partial core/validate core/with-default-messages))
(deftest validator-sets
  (testing "validator sets for nested maps"
    (is (core/valid? {:address {:postcode 2000
                                  :street   "Crown St"
                                  :country  "Australia"
                                  :past [{:country "Spain"} {:country "Brazil"}]}}
                     :address addr-validator-set))
    (is (not (core/valid? {}
                       :address addr-validator-set)))
    (let [errors-map {:address {
                                :postcode '("postcode must be present")
                                :street   '("street must be present")
                                :country  '("country must be present")
                                :past '("All items in past must satisfy the predicate")
                                }}
          invalid-map {:address {:postcode 
                                 :past [{:country nil} {:country "Brasil"}]}}]
      (is (= errors-map
             (first (default-validate invalid-map
                                   :address addr-validator-set))))))
  (testing "custom messages in validator sets"
    (let [errors-map {:address {
                                :postcode '("required")
                                :street    '("required")
                                :country   '("required")
                                :past '("every")
                                }}
          invalid-map {:address {:postcode 
                                 :past [{:country nil} {:country "Brasil"}]}}]
      (is (= errors-map
             (first (default-validate invalid-map
                                   :address addr-validator-set+custom-messages))))))
  (testing "validator sets and standard validators together"
    (let [errors-map {:age '("required")
                      :name '("name must be present")
                      :passport {:number '("number must be a positive number")}
                      :address {
                                :postcode '("postcode must be a number")
                                :street    '("street must be present")
                                :country   '("country must be present")
                                :past '("All items in past must satisfy the predicate")
                                }}
          invalid-map {:name nil
                       :age 
                       :passport {:number -7 :issued_by "Australia"}
                       :address {:postcode "NaN"
                                 :past [{:country nil} {:country "Brasil"}]}}]
      (is (= errors-map
             (first (default-validate invalid-map
                                   :name v/required
                                   :age [[(complement empty?) :message "required"]]
                                   [:passport :number] v/positive
                                   :address addr-validator-set))))))
  (testing "composing validator sets at the top level"
    (is (core/valid? {:address {:postcode 2000}
                      :name "Leo"
                      :age 29}
                     person-validator))
    (is (not (core/valid? {}
                          person-validator)))
    (let [errors-map {:address {:postcode '("postcode must be present")}
                      :name '("name must be present")
                      :age  '("age must be present")}]
      (is (= errors-map
             (first (default-validate {}
                                   person-validator)))))
    ;; Test for issue #7 on github
    (let [errors-map {:winner {:address {:postcode '("postcode must be present")}
                               :name '("name must be present")
                               :age  '("age must be present")}}]
      (is (= errors-map
             (first (default-validate {}
                                   deep-validator)))))))
(def valid-items #{:a :b :c})
(def items-validator-set
  {:field1 [[v/member valid-items]]})

addresses Issue https://github.com/leonardoborges/bouncer/issues/5

(deftest nested-symbols
  (testing "validator sets supports symbols -which are to be resolved- as arguments to validator functions"
    (is (not (core/valid? {:field1 :e}
                          items-validator-set)))
    (is (core/valid? {:field1 :a}
                          items-validator-set))))
(deftest boolean-validator
  (testing "value must be a boolean "
    (is (core/valid? {:active true} 
                     :active v/boolean))
    (is (core/valid? {:active false} 
                     :active v/boolean))
    (is (not (core/valid? {:active "false"} 
                          :active v/boolean)))
    (is (not (core/valid? {:active 0} 
                          :active v/boolean)))))
(deftest range-validator
  (testing "presence of value in the given range"
    (is (core/valid? {:age 4} 
                     :age [[v/in-range [0 5]]]))
    (is (not (core/valid? {:age 10}
                          :age [[v/in-range [0 9]]])))
    (is (core/valid? {:rating 3.7} 
                     :rating [[v/in-range [0 4]]]))
    (is (core/valid? {:rating 10.0}
                     :rating [[v/in-range [0 10]]]))
    (is (not (core/valid? {:rating 10.1}
                     :rating [[v/in-range [0 10]]])))))
(deftest member-validator
  (testing "presence of value in a collection"
    (is (core/valid? {:age 4}
                     :age [[v/member (range 5)]]))
    (is (not (core/valid? {:age 5}
                          :age [[v/member (range 5)]])))))
(deftest regex-validator
  (testing "matching the given pattern"
    (is (core/valid? {:phone "555"}
                  :phone [[v/matches #"^\d+$"]]))
    (is (not (core/valid? {:phone "NaN"}
                  :phone [[v/matches #"^\d+$"]])))))
(deftest email-validator
  (testing "can match typical legal emails"
    (is (core/valid? {:email "test@googlexyz.com"} :email [[v/email]]))
    (is (core/valid? {:email "test+blabla@googlexyz.com"} :email [[v/email]]))
    (is (core/valid? {:email "test@googlexyz.co.uk"} :email [[v/email]])))
  (testing "will reject invalid emails"
    (is (not (core/valid? {:email nil} :email [[v/email]])))
    (is (not (core/valid? {:email } :email [[v/email]])))
    (is (not (core/valid? {:email "test"} :email [[v/email]])))
    (is (not (core/valid? {:email "test@"} :email [[v/email]])))
    (is (not (core/valid? {:email "test@googlexyz"} :email [[v/email]])))
    (is (not (core/valid? {:email "@google.xyz.com"} :email [[v/email]])))))
(def y-m (f/formatters :year-month))
(deftest datetime-validator
  (testing "matched without custom formatter"
    (is (core/valid? {:dt "2014-04-02"} :dt [[v/datetime]]))
    (is (core/valid? {:dt "2014-04-02 03:03:03"} :dt [[v/datetime]])))
  (testing "rejected without custom formatter"
    (is (not (core/valid? {:dt "2014/04/01"} :dt [[v/datetime]]))))
  (testing "matched with custom formatter"
    (is (core/valid? {:dt "2014/04/01"} :dt [[v/datetime "yyyy/MM/dd"]])))
  (testing "valid date rejected because of specific clj-time formatter"
    (is (not (core/valid? {:dt "2014-01-02"} :dt [[v/datetime y-m]]))))
  (testing "matched by specific clj-time formatter"
    (is (core/valid? {:dt "2014-01"} :dt [[v/datetime y-m]]))))
(deftest max-count-validator
  (testing "enforcing a maximum value for count"
    (testing "with strings"
    (is (core/valid? {:first-name "First Name"} :first-name [[v/max-count 10]]))
    (is (not (core/valid? {:first-name "First Name"} :first-name [[v/max-count 9]]))))
    (testing "with collections"
    (is (core/valid? {:a-vector [1 2 3]} :a-vector [[v/max-count 3]]))
    (is (core/valid? {:a-list '(1 2 3)} :a-list [[v/max-count 3]]))
    (is (not (core/valid? {:a-map {:city "Atlanta" :state "Georgia"}} :a-map [[v/max-count 1]]))))))
(deftest min-count-validator
  (testing "enforcing a minimum value for count"
    (testing "with strings"
    (is (core/valid? {:password "password1"} :password [[v/min-count 8]]))
    (is (not (core/valid? {:password "open"} :password [[v/min-count 5]]))))
    (testing "with collections"
    (is (core/valid? {:a-vector [1 2 3]} :a-vector [[v/min-count 3]]))
    (is (core/valid? {:a-list '(1 2 3)} :a-list [[v/min-count 3]]))
    (is (not (core/valid? {:a-map {:city "Atlanta" :state "Georgia"}} :a-map [[v/min-count 3]]))))))

This file autogenerated from test/bouncer/validators_test.cljx