We might write perfect code but, unfortunately, our users are mere mortals and thus are prone to giving us cruddy data by mistake. If you recall, in Chapter 1, Getting Started with Luminus, we used the Luminus template to generate the hipstr application, which includes the lib-noir
library for us. One of the helper namespaces provided by lib-noir
is a noir.validation
namespace.
The noir.validation
namespace provides methods to validate data in a variety of ways. It includes functions to check whether or not an input is nil, is an e-mail, is of a certain minimum length, and so on. This is excellent because I hate writing validation code, and I'm sure you do, too.
However, while noir.validation
has a lot of functions that can be used to validate data, its actual validation framework makes some unfortunate assumptions as to how it will be used. It is also stateful, which makes it difficult to test and, frankly, is pretty unnecessary for a validation framework.
Instead of depending on the framework of noir.validation
and its way of doing things, we're going to use some of its validation functions in conjunction with Validateur
.
The Validateur
library is an alternative validation library to noir.validation
. Using Validateur
, we can define a collection of validators as a single function, which you can then apply against a map of key/values to be validated. The function will then return a map of all keys in the map that failed the validation and for each invalid key, a set of messages detailing why the validation failed (which is slightly similar to noir.validation
but without all the evils of maintaining the state). The validators that we'll construct with Validateur
are easily reusable and composable with other validators.
The website of Validateur
will provide a lot of information to you. The library is well documented and available for your perusal at http://clojurevalidations.info/.
The first thing that we need to do, as is the case with any third-party library we want to use, is import the library in our project.clj
file as a dependency of the app. Adjust the project's dependencies vector so it is similar to this:
(defproject... :dependencies [[org.clojure/clojure "1.6.0] ;...snipped for brevity [com.november/validateur "2.3.1"]] ;...rest of our project...
It can sometimes be difficult to determine where exactly one should write the validation code. Because one of the strengths of Validateur
is that validators can be created, reused, and composed together to create more complex validators, it makes sense for us to create the validators in appropriate namespaces.
In our case, the sign up form has three fields that require validation: :username
, :email
, and :password
. However, if we were to further extend this application to have something like a password reset form, then the e-mail and username validators would come in handy. As such, let's create a new namespace called hipstr.validators.user
and put our validators in it.
Create a new directory in our /src/hipstr
directory called validators
:
# mkdir validators
Create a new hipstr.validators.user-validator
namespace and ensure that it uses the validateur.validation
namespace:
(ns hipstr.validators.user-validator (:require [validateur.validation :refer :all]))
The key function in validateur.validation
is the validation-set
. The validation-set
function can be thought of as a factory responsible for creating and returning the actual validator to be applied to a map. The validation-set
function accepts a list of rules, which will ultimately be used to validate a map. The validator returns a set of error messages for any key whose value is invalid, otherwise it returns an empty map.
We can create a simple validator that checks for required fields using the presence-of
function rule:
(defn validate-signup [signup] "Validates the incoming map of values from our signup form, and returns a set of error messages for any invalid key. Expects signup to have :username, :email, and :password." (let [v (validation-set ;#1 (presence-of #{:username :email :password}))] ;#2 (v signup)))
In the preceding code, we created our own little validate-signup
function, which will take the parameters map from our POST/signup up route. At #1, we construct our validator by calling validation-set
, and we pass it to our first rule, presence-of
(as shown on #2), which itself accepts the set of keys that must be present, and truthy, in the map to be deemed valid (in our case, the sign-up map).
Ultimately, presence-of
returns a function (as do all the rules we pass to validation-set), which will be used by validation-set
during validation. These functions accept a map to be validated and perform their task against that map. We can take a peek into this at the REPL:
# lein repl hipstr.repl=> (require '[validateur.validation :refer :all]) nil hipstr.repl => (let [presence-of-fn (presence-of #=> #{:username :email :password})] #=> (presence-of-fn {:username "TheDude"})) [false {:password #{"can't be blank"}, :email #{"can't be blank"}}]
In the preceding REPL code, we load the validateur.validation
namespace and assign the result of presence-of
to our own function presence-of-fn
. We call presence-of-fn
and pass it the map to be validated. The validation fails because :password
and :email
are not provided.
It's easy to see how validation-set
simply iterates blindly over all of its validators, forwarding the map to each validator in question. Its simplicity is beautiful and elegant.
If we were to call validate-signup
from the REPL with an invalid map, we'd see the following:
hipstr.repl=> (use '[hipstr.validators.user-validator]) nil hipstr.repl=> (validate-signup {:username "TheDude"}) {:email #{"can't be blank"}, :password #{"can't be blank"}} hipstr.repl=> (validate-signup {:username "TheDude" :email "[email protected]" :password "12345678"}) {}
The default error message for presence-of
is "can't be blank
", however, we can provide our own message by passing the :message
keyword argument. Adjust the presence-of
's arguments to look like the following:
(presence-of #{:username :email :password} :message "is a required field")
If we re-execute from the REPL, we'll now see a different error message:
hipstr.repl=> (use 'hipstr.validators.user-validator :reload) nil hipstr.repl=> (validate-signup {:username "TheDude"}) {:email #{"is a required field."}, :password #{"is a required field."}}
Of course, checking for required fields isn't the only thing we need to ensure. We also need to ensure whatever values are supplied are in the correct format. I'm making the executive decision that our application will only allow a limited set of characters for usernames, because I'm old and grumbly.
We can use the
format-of
rule to ensure that a given value is appropriately formatted. Because the validator returned by validation-set
chains all the validation rules together, we can add the format-of
rule after the presence-of
rule. Adjust validate-signup
to be similar to the following:
;…snipped for brevity… (let [v (validation-set (presence-of #{:username :email :password) :message "is a required field.") (format-of :username :format #"^[a-zA-Z0-9_]*$" :message "Only letters, numbers, and underscores allowed."))])
The format-of
rule accepts the key whose value's format is to be validated (:username
), a regex pattern to which the value must conform, and an optional message if we don't want to use the default one provided (which in the case of format-of
is "has incorrect format"
).
Many of these functions take additional, optional parameters but for the sake of brevity, we aren't covering the entire suite. You can, as always, check out the docs at http://reference.clojurevalidations.info/validateur.validation.html#var-compose-sets.
At this point, if we try to validate our test data from the last example, we'll get the following response:
hipstr.repl=> (validate-signup {:username "The Dude" :email "[email protected]" :password "12345678"}) {:username #{"Only letters, numbers, and underscores allowed."}}
Excellent! So our format validator is working, but what happens if we don't pass :username
at all:
hipstr.repl=> (validate-signup {:email "[email protected]" :password "12345678"}) => {:username #{"is a required field" "can't be blank."}}
Huh, now that's interesting. Why did we get multiple required/blank validation messages for :username
? Our first validator, presence-of
, failed, so the message we provided was added to the error set for :username
. In addition, format-of
also does a check to ensure that the value is provided and adds its own validation message to the error set, can't be blank
. Considering format-of
already has its own check for existence, we may as well remove :username
from presence-of
, and change the default message for blank value validation in format-of
:
;...snipped for brevity... (let [v (validation-set (presence-of #{:email :password} :message "is a required field.") (format-of :username :format #"^[a-zA-Z0-9_]*$" :message "Only letters, numbers, and underscores allowed." :blank-message "is a required field"))])
Next up, we'll verify the password.
At this point, you might be starting to see a pattern, and it might come as no surprise that along with presence-of
and format-of
, there also exists length-of
. The length-of
rule can either check for an exact length or a range. Because we are software developers and know the importance of good passwords, we'll want a minimum length and a rather large maximum length. Let's add the length-of
rule to the list of rules in the validation-set
:
;…snipped for brevity… (length-of :password :within (range 8 101) :message-fn (fn [type m attribute & args] (if (= type :blank) "is a required field" "Passwords must be between 8 and 100 characters long.")))
In length-of
we provided :within
, which accepts a range. Alternatively, we could have specified an exact value using :is
. The major difference between length-of
and the other rules we've used so far is the :message-fn
argument. Because length-of
can fail for multiple reasons—it could fall outside our range, or not meet the exact value (if we had provided one), or be altogether blank—we can pass a function that determines the appropriate error message to return. From the docs:
:message-fn (default nil): function to retrieve message with signature (fn [type m attribute & args]). type will be :length:is or :length:within, args will be the applied number or range
Unfortunately, the docs are slightly misleading, as type
can also have the value :blank
. Hence, our :message-fn
function will return a "is a required field" message if the type
is :blank
, otherwise it will return a message that covers the rest the validation cases appropriately.
Lastly, we'll take a look at one more rule function to help us validate the format of our e-mail address.
E-mail addresses are hard to validate. Their regular expressions are difficult to maintain, ugly, and I'd rather let some other underlying library validate the e-mail address for me. Thankfully, one of the rules we can use is validate-with-predicate
.
The validate-with-predicate
rule takes a predicate and returns whatever the predicate returns. We can leverage this in conjunction with the noir.validation/is-email?
function to validate the e-mail.
First, add the noir.validation
requirement to the namespace:
(ns hipstr.validators.user (:require [validateur.validation :refer :all] [noir.validation :as v]))
Next, add the validate-with-predicate
rule to validation-set
, which will determine whether the e-mail is in the correct format:
;...snipped for brevity... (validate-with-predicate :email #(v/is-email? (:email %)) ;#1 :message-fn ;#2 (fn [validation-map] (if (v/has-value? (:email validation-map)) "The email's format is incorrect" "is a required field")))
At #1
, we define the predicate that will be used for this rule, which is just an anonymous function that passes the sign up map to the noir.validation/is-email?
function. The interesting bit takes place at #2
. Because validate-with-predicate
has no idea how the passed map is being validated, or why validation failed, we provide a :message-fn
funtion, which takes the entire map. We can then use the map to determine why the validation failed, and thus return an appropriate message. In our case, we know the e-mail's format is incorrect if a value was provided, otherwise we know it's blank. Our :message-fn
will simply return an appropriate message for the two cases.
At this point, the validate-signup
function should look like this:
(defn validate-signup [signup] "Validates the incoming map of values from our signup form, and returns a set of error messages for any invalid key. Expects signup to have :username, :email, and :password." (let [v (validation-set (presence-of #{:email :password} :message "is a required field.") (format-of :username :format #"^[a-zA-Z0-9_]*$" :message "Only letters, numbers, and underscores allowed." :blank-message "is a required field") (length-of :password :within (range 8 101) :message-fn (fn [type m attribute & args] (if (= type :blank) "is a required field" "Password must be between 8 and 100 characters long."))) (validate-with-predicate :email #(v/is-email? (:email %)) :message-fn (fn [validation-map] (if (v/has-value? (:email validation-map)) "the email's format is incorrect" "is a required field"))))] (v signup)))
Sometimes code formatting in a book is difficult to read. Here's a link to a gist to give your eyes a break: http://bit.ly/1rHZB0E
"But hold on! At the start of this section, you said that using Validateur
allows us to compose reusable validators! This validate-signup
can't be reused by anything but the signup form!" you may say. You are correct and we can fix this.
So far, we've created one big validator using the validateur.validation/validation-set
function. It works, but it's monolithic and can't be used by anything other than the signup form, which is unfortunate.
To open this up, we can use validateur.validation/compose-sets
, which instead of taking a collection of validator rules, takes a collection of validators returned by validateur.validation/validation-set
. This allows us to extract each of the validator rules in the validate-signup
function into its own validator using validation-set
, thus allowing us to reuse a single validator wherever we want.
First, let's extract each rule in the validate-signup
function into its own validator, such that our code looks like this:
(ns hipstr.validators.user (:require [validateur.validation :refer :all] [noir.validation :as v])) (def email-validator (validation-set (validate-with-predicate :email #(v/is-email? (:email %)) :message-fn (fn [validation-map] (if (v/has-value? (:email validation-map)) "The email's format is incorrect" "is a required field"))))) (def username-validator (validation-set (format-of :username :format #"^[a-zA-Z0-9_]*$" :blank-message "is a required field" :message "Only letters, numbers, and underscores allowed."))) (def password-validator (validation-set (length-of :password :within (range 8 101) :blank-message "is a required field." :message-fn (fn [type m attribute & args] (if (= type :blank) "is a required field" "Passwords must be between 8 and 100 characters long."))))) (defn validate-signup [signup] "Validates the incoming signup map and returns a set of error messages for any invalid field." (let [v (validation-set)] (v signup)))
In the previous code snippet, we created a single validator for each of our rules. Each of these validators can now be used by anything we want. Of course, now our validate-signup
doesn't really do anything. However, we can change this by swapping out validation-set
with compose-sets
, passing each of the previously defined validators and then immediately invoking it.
(defn validate-signup [signup] "Validates the incoming signup map and returns a set of error messages for any invalid field." ((compose-sets email-validator username-validator password-validator) signup))
A gist of the entire refactored namespace is available at http://bit.ly/1CsSrrd.
Our individual validators are now reusable and we can compose complex validators from them. Now, we just have to report errors to the user for any of the failed validations, or redirect them to the success page.
We already set up our route handler at the start of this chapter, such that, if there were any errors, we would re-render the signup.html
page with the user's submitted data along with the errors. Since we're using Selmer to render our templates, and because Selmer gracefully handles nil
to be an empty string, we can safely treat user's data and reported errors as though they were present.
Adjust the signup.html
page such that it looks like this:
{% extends "templates/base.html" %} {% block content %} <h1>Sign Up <span class="small">Nobody will ever know.</span></h1> <div class="row"> <div class="col-md-6"> <form role="form" method="post"> <div class="form-group"> <label for="username">Username</label> <ul class="errors"> {% for e in errors.username %} <!-- #1 --> <li>{{e}}</li> {% endfor %} </ul> <input type="input" name="username" class="form-control" id="username" placeholder="AtticusButch" value="{{username}}"> </div> <div class="form-group"> <label for="email">Email address</label> <ul class="errors"> {% for e in errors.email %} <li>{{e}}</li> {% endfor %} </ul> <input type="email" name="email" class="form-control" id="email" placeholder="[email protected]" value="{{email}}"> </div> <div class="form-group"> <label for="password">Password</label> <ul class="errors"> {% for e in errors.password %} <li>{{e}}</li> {% endfor %} </ul> <input type="password" name="password" class="form-control" id="password" placeholder="security-through-obscurity"> </div> <button type="submit" class="btn btn-default">Submit </button> </form> </div> </div> {% endblock %}
We are using Selmer's cycle tag (as shown at #1
)—which is actually a for
loop—to loop through any errors that exist for a given field and render out a fragment of HTML. We can do this because the validator that we created returns a set of error messages for any key whose validation failed; otherwise an empty map is returned, which Selmer can gracefully handle. We also were able to render out each respective input's value, if the user had already submitted one (so that they don't have to fill it in again, which would lead to them raging).
Finally, let's modify the hipstr.routes.home
namespace and the signup-page-submit
route handler we created, to make use of this validator.
First, add our user-validator
to the list of requirements as follows:
(:require ;...snipped for brevity... [hipstr.validators.user-validator :as v])
Then uncomment the let
form in our signup-page-submit
, and change the signup
alias to be v
instead:
(defn signup-page-submit [user] (let [errors (v/validate-signup user)] (if (empty? errors) (response/redirect "/signup-success") (layout/render "signup.html" (assoc user :errors errors)))))
The preceding Selmer template will now render the following page when any validation errors are present:
Finally, let's add a Cascade Style Sheet (CSS) style so that our error messages are a deeply foreboding red. CSS styles are stored in the /resources/public/css
folder. Out of the box, Luminus generates a screen.css
file for us, prewired and ready to go. We'll add our style to it:
.errors { padding-left: 20px; color: red; }
Save the file and refresh, and you'll have some urgent looking messages.
Finally, once we provide a valid username, e-mail, and password, we'll be redirected to the /signup-success
route, and our simple little message will be displayed:
18.225.72.245