Skip to content
The Clojure(Script) validation library you deserve.
Clojure
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
doc
src/heckle
test/heckle
.gitignore
.travis.yml
CHANGELOG.md
LICENSE
README.md
build.boot

README.md

heckle

The Clojure(Script) validation library you deserve.

Heckle is a flexible, function-oriented, and composable validations library for Clojure and ClojureScript.

Build Status Clojars Project

Why another validation library?

Clojure has several existing validation libraries, but as I looked around I was stunned to see how inflexibly they were designed. Many only support working with hash-maps. Many only support validating one piece of data in isolation (making it hard to validate things like a "Password confirmation" field). I couldn't find a validations library that elegantly addressed all of my validation needs, so I built one.

Usage

Let's dive right in with a quick example:

(require '[heckle.core :refer [validate]]
         '[heckle.validations :as v])

(def login-validations
  [(v/matches #".@." :email)
   (v/matches #"[a-z]" :password "must include a lower-case letter")
   (v/matches #"[A-Z]" :password "must include a capital letter")
   (v/matches #"\d" :password "must include a number")
   (v/length-is-at-least 8 :password)])

(validate login-validations {:email "me@example.com" :password "MyPa55word"})
  ; => {}

(validate login-validations {:email "me" :password "passwd"})
  ; => {:email #{"is invalid"}
  ;     :password #{"must include a capital letter"
  ;                 "must include a number"
  ;                 "must be at least 8 characters"}}

As you can see, heckle.core/validate take a list of "validations" and some data, and returns a hash-map of errors. The errors is a hash-map where the key is the invalid field and the value is a list of error messages. When everything is okay, the errors hash-map is empty.

All the hard work is handled by the validation functions you provide. The validation functions have a very simple interface: they take the data to validate as their only argument, and they return error information in the form of [error-key error-message] if the data fails the validation, or nil if the data passes.

The simplicity of the validation functions is the key difference between Heckle and other validation libraries. Since they receive the entire input data, they can check one field or many fields at will. The error key they return is independent from the data its validating.

Heckle ships with many standard validation functions built-in, and it's easy to define your own.

Built-in validation function builders

Heckle comes with functions to build your own validation functions. The heckle.validations namespace contains builder functions for validationg hash-map input data. Example usage:

(heckle.core/validate
  [(heckle.validations/is-present :email)
   (heckle.validations/is-confirmed :email)
   (heckle.validations/is-at-least 18 :age "is too young")]
  {:email "me@example.com" :age 30}) ; => {}
Fn name Description Arguments Default error message
is-present If value is a string, ensures it is not blank. If value is not a string, ensures it is not nil. key - the key whose value we will check
error-msg - (optional) custom error message when invalid
"is required"
matches Ensure a string matches against a regular expression regex - the regular expression
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
"is invalid"
is-one-of Ensure a value is one of a list of values collection - the set of permissible values
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be either A, B or C"
is-confirmed Ensure a value has been confirmed accurately (the value and its confirmation are equal) key - the key whose value we will check is confirmed
confirmation-key - (optional) the name of the key of the confirmation value
error-msg - (optional) custom error message when invalid
"does not match"
length-is-at-least Ensure a string or other sequence is at least the given length min-length - the minimum permissible length
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be at least 8 characters"
length-is-no-more-than Ensure a string or other sequence is at most the given length max-length - the maximum permissible length
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be less than 25 characters"
is-at-least Ensure a value is greater than or equal to a minimum value bound - the minimum permissible value
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be at least 3"
is-greater-than Ensure a value is strictly greater than a given value bound - the value above which valid values must be
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be greater than 2"
is-less-than Ensure a value is strictly less than a given value bound - the value below which valid values must be
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be less than 10"
is-no-more-than-than Ensure a value is less than or equal to a maximum value bound - the maximum permissible value
key - the key whose value we will check
error-msg - (optional) custom error message when invalid
e.g. "must be no more than 9"

Building your own validation functions

If none of the built-in validation functions work for you, it's easy to write your own validation functions. You can just write a your own validation from scratch (it's very easy!), or you can use Heckle's helper functions make-claim and make-denial.

Completely custom validations

A validation is just a function that accepts the input data as an argument and returns nil if everything is okay, or error information in the form [error-key error-message] if there's an error.

Here are some completely custom validation functions you can use with validate:

(def sign-up-validations
  [(fn [data] (when (empty? (:email data)) [:email "is required"]))
   (fn [data] (when-not (re-find #"[a-z]" (:password data)) [:password "must contain at least one lowercase letter"]))
   (fn [data] (when-not (re-find #"[A-Z]" (:password data)) [:password "must contain at least one capital letter"]))
   (fn [data] (when-not (re-find #"\d" (:password data)) [:password "must contain at least one number"]))])

(heckle.core/validate sign-up-validations {:email "" :password "pass"})
  ; => {:email #{"is required"}
  ;     :password #{"must conatin at least one capital letter"
  ;                 "must contain at least one number"}}

Using make-claim and make-denial

It's even easier to write custom validation functions using two helper functions make-claim and make-denial. These functions both take a predicate function, the error key and the error message. make-claim expects the predicate to return a truthy value when there is no error, and make-denial expects the predicate to return a falsey value when there is no error.

Let's see the same example as above, but using these helper functions:

(def sign-up-validations
  [(heckle.core/make-denial #(empty? (:email %1)) :email "is required")
   (heckle.core/make-claim #(re-find #"[a-z]" (:password %1)) :password "must contain at least one lowercase letter")
   (heckle.core/make-claim #(re-find #"[A-Z]" (:password %1)) :password "must contain at least one capital letter")
   (heckle.core/make-claim #(re-find #"\d" (:password %1)) :password "must contain at least one number")])

(heckle.core/validate sign-up-validations {:email "" :password "pass"})
  ; => {:email #{"is required"}
  ;     :password #{"must conatin at least one capital letter"
  ;                 "must contain at least one number"}}

Short-circuiting validations with validation groups

Normally, Heckle will run every validation you give it. This is good if you want your user to know everything they need to fix. But sometimes it's preferable to stop early if you encounter an error and skip subsequent validations.

For this, Heckle provides the function heck.core/group. This function takes a list of validation functions and returns a validation function that lazily evaluates the given validations until one returns an error or all have passed.

(def sign-up-validations
  [(heckle.core/group
     (heckle.validations/is-present :email)
     (heckle.validations/matches #".@." :email))
   (heckle.core/group
     (heckle.validations/is-present :password)
     (heckle.validations/length-is-at-least 8 :password)
     (heckle.core/group
       (heckle.validations/matches #"[A-Z]" :password "must include a capital letter")
       (heckle.validations/matches #"[a-z]" :password "must include a lower-case letter")
       (heckle.validations/matches #"\d" :password "must include a number")))])

(heckle.core/validate sign-up-validations {:email "" :password ""})
  ; => {:email #{"is required"}
  ;     :password #{"is required"}}
(heckle.core/validate sign-up-validations {:email "me@example.com" :password "pass"})
  ; => {:password #{"must be at least 8 characters" "must include a capital letter"}}

As you can see, groups can be nested as much as you want. That's because groups are just validation functions themselves that execute other validation functions. Functional composition ftw!

This is useful for a number of use cases:

  • You want at most only 1 error message per field
  • You want at most only 1 error message entirely
  • Certain validations are dependent on other validations having passed (like if a validation function will raise an error if given nil or a datetime check will raise if the input string doesn't parse)
  • Certain validations are expensive to compute and you'd rather skip them if something else is wrong anyway (e.g. they involve a network call)

Roadmap

  • Add collection validations
  • Add a syntax shortcut for setting up multiple validations for the same field
  • Allow customization of the default error messages
  • Support records in addition to hash-maps for built-in validation functions
  • Add data type validations?

License

Copyright © 2018 Nathan Wallace

Distributed under the MIT License.

You can’t perform that action at this time.