Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
A validation DSL for Clojure & Clojurescript applications
Clojure

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
docs
src/bouncer
test/bouncer
.gitignore
CHANGELOG.md
README.md
project.clj

README.md

bouncer

A tiny Clojure library for validating maps (or records).

Table of Contents

Motivation

Check this blog post where I explain in detail the motivation behind this library

Setup

If you're using leiningen, add it as a dependency to your project:

[bouncer "0.2.0"]

Or if you're using maven:

<dependency>
  <groupId>bouncer</groupId>
  <artifactId>bouncer</artifactId>
  <version>0.2.0</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:

Usage

Basic validations

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")}}]

Validating nested maps

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.

Multiple validation errors

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.

Validating collections

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.

Composability: validator sets

If you find yourself repeating a set of validators over and over, chances are you will want to encapsulate that 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 ""}}]

Customization Support

Custom validations using arbitrary functions

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}]

Writing validators

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 than nil. 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"))

Validators and arbitrary number of arguments

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 in 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.

Built-in validations

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)

Contributing

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.

TODO

  • Allow defvalidatorset to encapsulate top level validator sets - including nested sets
  • Add more validators (help is appreciated here)

CONTRIBUTORS

License

Copyright © 2012 Leonardo Borges

Distributed under the MIT License.

Something went wrong with that request. Please try again.