DEV Community


Posted on

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:&& (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* "")
;; =>

(funcall *validator* "asdf")
;; =>
"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.
  (let ((messages nil)
        (valid t))
    (loop for validator in validators
          if (and (eql :allow-blank validator)
                  (str:blankp object))
            return t
            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)

Top comments (0)