(comp-validators & validators)

Creates a validator that is a composition of the validators passed as the arguments:

(def not-empty [:not-empty (fn [v] (not (empty? v)))])

(def username-validator (validator {:username [not-empty]}))
(def password-validator (validator {:password [not-empty]}))

(def user-validator (comp-validators username-validator password-validator))

(user-validator {:username "" :password ""})
;; returns {:username {:$errors$ {:value "" :failed [:not-empty]}}
;;          :password {:$errors$ {:value "" :failed [:not-empty]}}}


(validator validators)

Creates a form validator. Validator is a map where keys represent the path to data that will be validated and the value is a vector of attribute validators.

Attribute validators are tuples where the first element is the attribute validator name and the second one is the validation function. Validation function receives the value for the key path and returns a boolean. It should return true if the attribute is valid and false if it’s invalid. Attribute validators receive full-data (whole object that is being validated) and the attribute path as the second and third arguments.

Example attribute validator

(def not-empty [:not-empty ;; Name of the attribute validator
                (fn [value _ _]
                  (not (empty? v)))]

If you want to build more complex validators full-data and path arguments allow you to do so. For instance, let’s say we’re writing the validator that can check if the email confirmation is the same as the email:

(def email-confirmation [:confirmed-email?
                         (fn [value full-data path]
                           (let [email (:email full-data)
                                 email-confirmation (:email-confirmation full-data)]
                             (= email email-confirmation)))]

validator returns the function that accepts the data and returns the map of validation errors.

Simple example:

(def not-empty [:not-empty (fn [v _ _] (not (empty? v)))])
(def form-validator-1 (validator {:username [not-empty]}))

(form-validator-1 {:username ""})
;; returns {:username {:$errors$ {:value "" :failed [:not-empty]}}}

Validators can validate nested paths:

(def form-validator-2 (validator { [not-empty]}))
(form-validator-2 {:user {:username ""}})
;; returns {:user {:username {:$errors$ {:value "" :failed [:not-empty]}}}}

Validators can validate objects in the list:

(def form-validator-3 (validator {:user.accounts.*.network [not-empty]}))
(form-validator-3 {:user {:accounts [{:network ""}]}})
;; returns {:user {:accounts {0 {:network {:$errors$ {:value "" :failed [:not-empty]}}}}}}

Validators can validate values in the list:

(def form-validator-4 (validator {* [not-empty]}))
(form-validator-3 {:user {:phone-numbers [""]}})
;; returns {:user {:phone-numbers {0 {:$errors$ {:value "" :failed [:not-empty]}}}}}

Validators can be nested inside other validators:

(def user-validator (validator {:username [not-empty]}))
(def article-validator (validator {:title [not-empty]
                                   :user [user-validator]}))

(article-validator {:title "" :user {:username ""}})
;; returns {:title {:$errors {:value "" :failed [:not-empty]}}
;;          :user {:username {:$errors$ {:value "" :failed [:not-nil]}}}}

Features provided by the validator ensure that you can validate any data structure, no matter how deeply nested it is. You can also create small focused validators that can be nested or composed which ensures that your validation logic stays DRY and allows reuse of the validators.