A validation DSL for Clojure apps
- Annotated Source
- Motivation
- Setup
- Usage
- Composability: validator sets
- Customization support
- Built-in validators
- Contributing
- TODO
- CHANGELOG
- CONTRIBUTORS
- License
Check this blog post where I explain in detail the motivation behind this library
If you're using leiningen, add it as a dependency to your project:
[bouncer "0.2.3-beta4"]
Or if you're using maven:
<dependency>
<groupId>bouncer</groupId>
<artifactId>bouncer</artifactId>
<version>0.2.3-beta4</version>
</dependency>
Then, require the library:
(require '[bouncer [core :as b] [validators :as v]])
bouncer provides two main macros, validate
and valid?
valid?
is a convenience function built on top of validate
:
(b/valid? {:name nil}
:name v/required)
;; false
validate
takes a map and one or more validation forms and returns a vector.
The first element in this vector contains a map of the error messages, whereas the second element contains the original map, augmented with the error messages.
Let's look at a few examples:
Below is an example where we're validating that a given map has a value for both the keys :name
and :age
.
(require '[bouncer [core :as b] [validators :as v]])
(def person {:name "Leo"})
(b/validate person
:name v/required
:age v/required)
;; [{:age ("age must be present")}
;; {:name "Leo", :bouncer.core/errors {:age ("age must be present")}}]
As you can see, since age is missing, it's listed in the errors map with the appropriate error messages.
Error messages can be customized by providing a :message
option - e.g: in case you need them internationalized:
(b/validate person
:age (v/required :message "Idade é um atributo obrigatório"))
;; [{:age ("Idade é um atributo obrigatório")}
;; {:name "Leo", :bouncer.core/errors {:age ("Idade é um atributo obrigatório")}}]
Nested maps can easily be validated as well, using the built-in validators:
(def person-1
{:address
{:street nil
:country "Brazil"
:postcode "invalid"
:phone "foobar"}})
(b/validate person-1
[:address :street] v/required
[:address :postcode] v/number
[:address :phone] (v/matches #"^\d+$"))
;;[{:address
;; {:phone ("phone must match the given regex pattern"),
;; :postcode ("postcode must be a number"),
;; :street ("street must be present")}}
;; {:bouncer.core/errors {:address {
;; :phone ("phone must match the given regex pattern"),
;; :postcode ("postcode must be a number"),
;; :street ("street must be present")}},
;; :address {:country "Brazil", :postcode "invalid", :street nil,
;; :phone "foobar"}}]
In the example above, the vector of keys is assumed to be the path in an associative structure.
bouncer
features a short circuit mechanism for multiple validations within a single field.
For instance, say you're validating a map representing a person and you expect the key :age
to be required
, a number
and also be positive
:
(b/validate {:age nil}
:age [v/required v/number v/positive])
;; [{:age ("age must be present")} {:bouncer.core/errors {:age ("age must be present")}, :age nil}]
As you can see, only the required
validator was executed. That's what I meant by the short circuit mechanism. As soon as a validation fails, it exits and returns that error, skipping further validators.
However, note this is true within a single map entry. Multiple map entries will have all its messages returned as expected:
(b/validate person-1
[:address :street] v/required
[:address :postcode] [v/number v/positive])
;; [{:address {:postcode ("postcode must be a number"), :street ("street must be present")}} {:bouncer.core/errors {:address {:postcode ("postcode must be a number"), :street ("street must be present")}}, :address {:country "Brazil", :postcode "invalid", :street nil, :phone "foobar"}}]
Also note that if we need multiple validations against any keyword or path, we need only provide them inside a vector, like [v/number v/positive]
above.
Sometimes it's useful to perform simple, ad-hoc checks in collections contained within a map. For that purpose, bouncer provides every
.
Its usage is similar to the validators seen so far. This time however, the value in the given key/path must be a collection (vector, list etc...)
Let's see it in action:
(def person-with-pets {:name "Leo"
:pets [{:name nil}
{:name "Gandalf"}]})
(b/validate person-with-pets
:pets (v/every #(not (nil? (:name %)))))
;;[{:pets ("All items in pets must satisfy the predicate")}
;; {:name "Leo", :pets [{:name nil} {:name "Gandalf"}],
;; :bouncer.core/errors {:pets ("All items in pets must satisfy the predicate")}}]
All we need to do is provide a predicate function to every
. It will be invoked for every item in the collection, making sure they all pass.
Note that if a map if pipelined through multiple validators, bouncer will leave it's errors map untouched and simply add new validation errors to it:
(-> {:age "NaN"}
(core/validate :name v/required)
second
(core/validate :age v/number)
second
::core/errors)
;; {:age ("age must be a number"), :name ("name must be present")}
Validators can take a pre-condition option :pre
that causes it to be executed only if the given pre-condition - a truthy function - is met.
Consider the following:
(core/valid? {:a -1 :b "X"}
:b (v/member #{"Y" "Z"} :pre (comp pos? :a)))
;; true
As you can see the value of b
is clearly not in the set #{"Y" "Z"}
, however the whole validation passes because the v/member
check states is should only be run if :a
is positive.
Let's now make it fail:
(core/valid? {:a 1 :b "X"}
:b (v/member #{"Y" "Z"} :pre (comp pos? :a)))
;; false
If you find yourself repeating a set of validators over and over, chances are you will want to group and compose them somehow. The macro bouncer.validators/defvalidatorset
does just that:
(use '[bouncer.validators :only [defvalidatorset]])
;; first we define the set of validators we want to use
(defvalidatorset addr-validator-set
:postcode [v/required v/number]
:street v/required
:country v/required)
;;just something to validate
(def person {:address {
:postcode ""
:country "Brazil"}})
;;now we compose the validators
(b/validate person
:name v/required
:address addr-validator-set)
;;[{:address
;; {:postcode ("postcode must be a number" "postcode must be present"),
;; :street ("street must be present")},
;; :name ("name must be present")}
;;
;; {:bouncer.core/errors {:address {:postcode ("postcode must be a number" "postcode must be present"),
;; :street ("street must be present")}, :name ("name must be present")},
;; :address {:country "Brazil", :postcode ""}}]
Validator sets can also be composed together and used as a top level validation as shown below:
(defvalidatorset address-validator
:postcode v/required)
(defvalidatorset person-validator
:name v/required
:age [v/required v/number]
:address address-validator)
(core/validate {}
person-validator)
;;[{:address {:postcode ("postcode must be present")}, :age ("age must be present"), :name ("name must be present")} {:bouncer.core/errors {:address {:postcode ("postcode must be present")}, :age ("age must be present"), :name ("name must be present")}}]
Much like the collections validations above, bouncer gives you the ability to use arbitrary functions as predicates for validations through the custom
built-in validator. Its usage should be familiar:
(defn young? [age]
(< age 25))
(b/validate {:age 29}
:age (v/custom young? :message "Too old!"))
;; [{:age ("Too old!")}
;; {:bouncer.core/errors {:age ("Too old!")}, :age 29}]
Another way - and the preferred one - to provide custom validations is to use the macro defvalidator
in the bouncer.validators
namespace.
The advantage of this approach is that your validator can be used in the same way built-in validators are - there's no need to use bouncer.validators/custom
.
As an example, here's a simplified version of the bouncer.validators/number
validator:
(use '[bouncer.validators :only [defvalidator]])
(defvalidator my-number-validator
{:default-message-format "%s must be a number"}
[maybe-a-number]
(number? maybe-a-number))
defvalidator
takes your validator name, an optional map of options and the body of your predicate function.
Options is a map of key/value pairs where:
:default-message-format
- to be used when clients of this validator don't provide one:optional
- a boolean indicating if this validator should only trigger for keys that have a value different thannil
. Defaults to false.
Using it is then straightforward:
(b/validate {:postcode "NaN"}
:postcode my-number-validator)
;; [{:postcode ("postcode must be a number")}
;; {:bouncer.core/errors {:postcode ("postcode must be a number")}, :postcode "NaN"}]
As you'd expect, the message can be customized as well:
(b/validate {:postcode "NaN"}
:postcode (my-number-validator :message "must be a number"))
Your validators aren't limited to a single argument though.
Since v0.2.2, defvalidator
takes an arbitrary number of arguments. The only thing you need to be aware is that the value being validated will always be the first argument you list. Let's see an example with the member
validator:
(defvalidator member
[value coll]
(some #{value} coll))
Yup, it's that simple. Let's use it:
(def kid {:age 10})
(b/validate kid
:age (member (range 5)))
In the example above, the validator will be called with 10
- that's the value the key :age
holds - and (0 1 2 3 4)
- which is the result of (range 5)
and will be fed as the second argument to the validator.
I didn't spend a whole lot of time on bouncer so it only ships with the validations I've needed myself. At the moment they live in the validators namespace:
-
bouncer.validators/required
-
bouncer.validators/number
-
bouncer.validators/positive
-
bouncer.validators/member
-
bouncer.validators/matches
(for matching regular expressions) -
bouncer.validators/custom
(for ad-hoc validations) -
bouncer.validators/every
(for ad-hoc validation of collections. All items must match the provided predicate)
Pull requests of bug fixes and new validators are most welcome.
Note that if you wish your validator to be merged and considered built-in you must implement it using the macro defvalidator
shown above.
Feedback to both this library and this guide is welcome.
- Add more validators (help is appreciated here)
- Docs are getting a bit messy. Fix that.
Copyright © 2012 Leonardo Borges
Distributed under the MIT License.