Arianna is a Clojure data validation library. Specifically, it's a fork of Stuart Sierra's excellent validate library. That said, I'm not averse to ripping off code and good ideas from other validation libraries including Validateur and libnoir.
Most of this library consists of tools to create and compose
validators. Actual validation logic is just handled by normal
Clojure functions with no special rules so, for instance, if
you want to check something is a string, you just use string?
.
If you want to check the keys of a map, you just call keys
. There
are a couple of useful functions such as v/email?
for common
cases.
Note that you can compose validators, so the result of one validator becomes the input to the next.
ALPHA code: Even with ripping off Stuart Sierra's design, this is a young library. Pretty much the entire API changed between 0.1 and 0.2, although the philosphy has not. It's just that 0.2 achieves its aims better than 0.1.
Available on Clojars
[net.colourcoding/arianna "0.2.1"]
You can read the full API docs, but I'd recommend finishing this readme first.
Here's a fully worked example (we'll explain how it works later)
(require '[arianna :as v])
(v/summarize
(v/and-all (v/->> keys (v/are keyword?))
(v/->> values (v/are string?))
(v/->> :email v/optional
email? "{{value}} is not a valid email.")
(v/->> :name v/require "You must enter a name.")
(v/->> :message v/require "You must enter a message."
count (> % 20) "Message must be longer than {{validator.y}} characters."))
input)
will give a result something like
{:email ["xxx is not a valid email"]
:name ["You must enter a name"]
:message ["Message must be longer than 20 characters."]}
Or nil
if the input passes (making the result suitable for use within if-let
).
If you want the full data structure for what failed, you call
v/validate
instead. You can call v/valid?
on that result
to check if it passes.
The following is a quick introduction to the libraries. More details are available in the API docs.
N.B. It's well worth checking out the unit tests to see how the library is used.
The primitive validation macros are: is
, is-not
and are
. They transform boolean predicate function into a validator:
(def number-validator (v/is number?))
(v/validate number-validator 42)
;;=> {:status :ok
:result 42
:errors nil
:input 42}
(number-validator "hi")
;;=> {:status :error
:result nil
:errors [{:validator number-validator :value "hi"}]
:input "hi"}
If the function doesn't take the input as the last parameter, you
can indicate its position with %
.
(defn less-than-10 (v/is < % 10))
has
takes a keyword or a vector, k
, and expands to
(v/is contains? % k)
or (v/is contains-in? % k)
as appropriate.
Single element vectors are stripped to their value.
Finally, there is is-optional
, which is only really useful in
conjunction with v/->>
. There's more information in the API
docs.
There's an equivalent macro for transform validations: as
.
(v/validate (v/as v/number) "4")
;;=> {:status :ok, :result 4, :errors nil, :input "4"}
There is also an equivalent of has
, as-key
. as-key
always
passes, and returns the special value :arianna/missing
if the
key is not present.
The and
function combines multiple validation functions into one.
This is not the same as clojure.core/and
, but it does short-circuit
the same way:
(def odd-integer
(v/and (v/is integer?)
(v/is odd?)))
(odd-integer 4)
;;=> ({:expected (v/is odd?), :value 4})
(odd-integer 4.0)
;;=> ({:expected (v/is integer?), :value 4.0})
and-all
does the same, but does not short circuit. This is better
for when you want to provide more feedback and less useful when later
feedback would be redundant.
There's also a short syntax which makes it a lot more convenient:
(def odd-integer (v/and integer? odd?))
You can also follow validators with strings to provide them with
human-readable feedback through v/summarize
.
(def odd-integer (v/and integer? "Should be an integer."
odd? "Should be odd."))
It also supports or
, which works as you might imagine.
Here's the interesting bit: you can compose transform validators.
(v/->> :email v/required v/email?)
is equal to
(v/->> (v/as-key :email)
(v/is-not v/absent? #{:missing :nil :blank})
(v/is v/email?))
The every
function transforms a list of validators into a
validator that operates on each element of a collection:
(v/validate (v/every even?) [4 3 8 15])
Will give two errors, one keyed by 3 and one by 15.
The are
macro is a synonym for every
.
are
composes effectively with the threading macro to give more capabilities. For map-like collections, you can thread keys
and vals
to apply validations to the keys and values of the map, respectively:
(def are-string (v/are string?))
(def simple-map
(v/and (v/->> keys (v/are keyword?))
(v/->> vals are-string)))
(v/validate simple-map {:a "one", :b 2})
;;=> {:errors ({:validator (v/is string?), :value 2})
You can also use count
to validate facts about the number of
elements in a collection:
(v/validate (v/->> count (< % 4))) [:a :b :c :d :e])
or, for that matter, strings.
(v/validate (/->> count (in-range? 3 10)) "password")
There are two branching constructs: cond
and when
. cond
takes
pairs of test and transform validators.
A validator is a data structure. When you call (v/validate validator input)
it will return a validation result, a record with the following fields
:result
The result of the transformation, or nil if the validator is a straight predicate.:input
The input to the validator:status
Either:ok
or:error
. You're better off calling(v/valid? validation-result)
than using this directly.:errors
A sequence of validation errors.
Validation errors are
:validator
The validator that failed.:value
The value that caused the failure:chain
The execution chain that gave rise to the value. This a sequence of validation results assoced with the :validator that gave rise to them.
Exceptions will be caught by v/validate
and v/summarize
and
treated as validation failures. The validation error will have
a key :exception
which contains the original exception.
v/debug-validate
doesn't catch the exceptions and is useful
on the repl.
Let's say you've got a map
(defn u {:user "moomin" :password "zog"})
and a validator
(def pass (v/->> :password
count
(in-range? 6 10)
"Password length must be between {{validator.min-incl}} characters and {{validator.max-excl}}."))
The string is associated with the previous validator. The messages
are mustache templates applied to validation errors keyed by the
validator. {{value}}
is the value entered into the validator.
{{validator.min-incl}}
is the min-incl
parameter to the
function. If that doesn't make you choose decent parameter
names, nothing will.
The v/summarize
function can be used to process these strings:
(v/summarize pass u)
;;; {:password ["Password length must between 6 and 10."]}
You can associate any information you like with the validator by
either a) just calling assoc on a primitive or b) putting
a hash-map after it in a composite. If you want to override the
key, you set the :arianna/field
key. This is pretty useful
where the system can't deduce the field.
(defn matching-fields? [f1 f2 input]
(= (get f1 input) (get f2 input)))
(def repeat-email
(v/->> (matching-fields? :email :repeat-email)
"Email and Repeat Email fields don't match."
{:arianna/field :repeat-email}))
If you need to validate internal data, rather than business data, the assert-valid
macro can be useful. It throws an exception if the
validation fails:
(v/assert-valid (/ 22.0 7.0) (v/is integer?))
;; #<ExceptionInfo clojure.lang.ExceptionInfo: Validation failed ...>
Get the error messages out from ex-data
:
(ex-data *e)
;;=> {:errors ({:value 3.142857142857143,
;; :validator (v/is integer?)}),
;; :line 1,
;; :expr (/ 22.0 7.0),
;; :value 3.142857142857143,
;; :file "NO_SOURCE_PATH"}
When the validation passes, assert-valid
returns the input value,
making it suitable for pipelining:
(-> (rand-int 100)
(* 2)
inc
(v/assert-valid (v/is odd?)))
;;=> 145
All pull requests, issues and conversations are welcome, but I do ask that any changes come with tests that demonstrate the original problem.
The library aims to allow to
- use normal clojure functions to perform validation
- give you a full trace of any failures
- validate anything, but provide useful features for validating maps from web forms and json posts.
What it explicitly doesn't want to be, is a type system like Schema.
- Support ClojureScript
- Support asynchronous validation
Obviously, async is likely to result in a serious API change.
Honestly, to scratch an itch. However, the biggest difference is that validate and Validateur represent validators as functions. This in turn means that feedback is ad-hoc. Arianna is me trying to come up with a regular way of dealing with feedback. It's not completely successful at the present time. Combining the ability to use any function and still have things be data driven is tricky.
libnoir has some useful functionality arianna currently lacks. I'm planning to rectify that by stealing the code.
It contains compatibility functions with "validate". It's mostly useful to see how to move between one and the other. All the functions are unit tested, so they can be used, but I think the native syntax is preferable.
Copyright (c) Stuart Sierra, Julian Birch 2013. All rights reserved. The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can be found in the file epl-v10.html at the root of this distribution. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software.