Form validation in Common Lisp
Tags: lisp, commonlisp
Published: 2024-02-28 11:07:18

I've been using the [clavier](https://github.com/mmontone/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](https://github.com/mmontone/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:

```lisp

(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!")))
```

You can compose them with boolean logic:

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

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

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

(funcall *validator* "asdf")
;; =>
NIL
"Length of \"asdf\" is less than 10"
```

For one, I want a shorter construct for this common need. [My PR](https://github.com/mmontone/clavier/pull/10) was rejected so here it is.

Use a `:allow-blank` keyword:

```lisp
(defmethod validators ((obj (eql 'book)))
  (dict 'isbn (list :allow-blank
                    (clavier:len :min 10 :max 13
                    …
```

and write a `validate-all` function:

```lisp
(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)))))
```

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)