Form validation in Common Lisp

vindarel

vindarel

Posted on February 28, 2024

Form validation in Common Lisp

I've been using the clavier library for input validation, it works nicely but we could make it a bit more terse.

Let's say you are building many HTML forms. Doing one all manually is OK-ish, not two. You could use cl-forms (I didn't, I'm building a layer to get a form from Mito objects. If you didn't see where I'm doing it look better or stay tuned ;) ) You could do things semi-manually and use Clavier for input validation. It works like this.

Define a list of validators for your fields:


(defmethod validators ((obj (eql 'book)))
  (dict 'isbn (list ;; other validator here…
                    (clavier:len :min 10 :max 13
                                 ;; :message works with clavier's commit of <2024-02-27>
                                 ;; :message "an ISBN must be between 10 and 13 characters long"
                                 ))
        'title (clavier:~= "test"
                           "this title is too common, please change it!")))
Enter fullscreen mode Exit fullscreen mode

You can compose them with boolean logic:

(defparameter *validator* (clavier:||
                                   (clavier:blank)
                                   (clavier:&& (clavier:is-a-string)
                                               (clavier:len :min 10)))
  "Allow a blank value. When non blank, validate.")
Enter fullscreen mode Exit fullscreen mode

This validator allows an input to be an empty string, but if it isn't, it validates it.

(funcall *validator* "")
;; =>
T
NIL

(funcall *validator* "asdf")
;; =>
NIL
"Length of \"asdf\" is less than 10"
Enter fullscreen mode Exit fullscreen mode

For one, I want a shorter construct for this common need. My PR was rejected so here it is.

Use a :allow-blank keyword:

(defmethod validators ((obj (eql 'book)))
  (dict 'isbn (list :allow-blank
                    (clavier:len :min 10 :max 13
                    
Enter fullscreen mode Exit fullscreen mode

and write a validate-all function:

(defun validate-all (validators object)
  "Run all validators in turn. Return two values: the status (boolean), and a list of messages.

  Allow a keyword validator: :allow-blank. Accepts a blank value. If not blank, validate."
  ;; I wanted this to be part of clavier, but well.
  ;; https://github.com/mmontone/clavier/pull/10
  (let ((messages nil)
        (valid t))
    (loop for validator in validators
          if (and (eql :allow-blank validator)
                  (str:blankp object))
            return t
          else
            do (unless (symbolp validator)
                 (multiple-value-bind (status message)
                     (clavier:validate validator object :error-p nil)
                   (unless status
                     (setf valid nil))
                   (when message
                     (push message messages)))))
    (values valid
            (reverse (uiop:ensure-list messages)))))
Enter fullscreen mode Exit fullscreen mode

This could be made better for a library API maybe? Anyways it works for now©.

See also that Clavier has a "validator-collection" thing, but not shown in the README, and is again too verbose in comparison to a simple list, IMO.

that's it, see ya next time.

Appendix: validators list:

This is the list of available validator classes and their shortcut function:

  • equal-to-validator (==)
  • not-equal-to-validator (~=)
  • blank-validator (blank)
  • not-blank-validator (not-blank)
  • true-validator (is-true)
  • false-validator (is-false)
  • type-validator (is-a type)
  • string-validator (is-a-string)
  • boolean-validator (is-a-boolean)
  • integer-validator (is-an-integer)
  • symbol-validator (is-a-symbol)
  • keyword-validator (is-a-keyword)
  • list-validator (is-a-list)
  • function-validator (fn function message)
  • email-validator (valid-email)
  • regex-validator (matches-regex)
  • url-validator (valid-url)
  • datetime-validator (valid-datetime)
  • pathname-validator (valid-pathname)
  • not-validator (~ validator)
  • and-validator (&& validator1 validator2)
  • or-validator (|| validator1 validator2)
  • one-of-validator (one-of options)
  • less-than-validator (less-than number)
  • greater-than-validator (greater-than number)
  • length-validator (len)
  • :allow-blank (not merged, only in my fork)
💖 💪 🙅 🚩
vindarel
vindarel

Posted on February 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related