GithubHelp home page GithubHelp logo

bouncer's Introduction

bouncer Travis CI status

A validation DSL for Clojure apps

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

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.

Validation pipelining

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

Pre-conditions

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

Composability: validator sets

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

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

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

  • Add more validators (help is appreciated here)
  • Docs are getting a bit messy. Fix that.

CONTRIBUTORS

License

Copyright © 2012 Leonardo Borges

Distributed under the MIT License.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.