Forms:   Web Form Validation
1 Introduction
1.1 Tutorial
1.1.1 Validation
1.1.2 Presentation
1.1.3 Nested Validation
1.1.4 Next Steps
2 Reference
2.1 Forms
form
form*
form-validate
form-run
2.2 Formlets
binding/  file
binding/  text
binding/  boolean
binding/  email
binding/  number
binding/  symbol
2.2.1 Primitives
ok
ok?
err
err?
ensure
2.2.2 Validators
required
matches
one-of
shorter-than
longer-than
2.3 Widgets
widget-input
widget-errors
widget-checkbox
widget-email
widget-file
widget-hidden
widget-number
widget-password
widget-select
widget-radio-group
widget-text
widget-textarea
widget-namespace
2.3.1 Contracts
attributes/  c
errors/  c
radio-options/  c
select-options/  c
widget/  c
widget-renderer/  c
7.9

Forms: Web Form Validation

Bogdan Popa <bogdan@defn.io>

 (require forms) package: forms-lib

1 Introduction

This library lets you declaratively validate web form data. It differs from the formlets provided by Formlets: Functional Form Abstraction in two important ways:

1.1 Tutorial

1.1.1 Validation

forms are composed of other forms and formlets. A basic form might look like this:

(define simple-form
  (form
    (lambda (name)
      (and name (string-upcase name)))
    (list (cons 'name binding/text))))

This form accepts an optional text value named "name" and returns its upper-cased version. To validate some data against this form we can call form-validate:

> (form-validate simple-form (hash))

'(ok . #f)

> (form-validate simple-form (hash "name" (make-binding:form #"name" #"Bogdan")))

'(ok . "BOGDAN")

Formlets can be chained in order to generate more powerful validations. If we wanted the above form to require the "name" field, we’d combine binding/text with required using ensure:

(define simple-form
  (form
    (lambda (name)
      (string-upcase name))
    (list (cons 'name (ensure binding/text (required))))))

If we validate the same data against simple-form now, our results differ slightly:

> (form-validate simple-form (hash))

'(err (name . "This field is required."))

> (form-validate simple-form (hash "name" (make-binding:form #"name" #"Bogdan")))

'(ok . "BOGDAN")

Notice how in the first example, an error was returned instead of #f and we no longer need to guard against false values in our lambda.

So far so good, but the syntax used to declare these forms can get unwieldy as soon as your forms grow larger than a couple fields. The library provides form*, which is a convenience macro designed to make writing large forms more manageable. In day-to-day use, you’d declare the above form like this:

(define simple-form
  (form* ([name (ensure binding/text (required))])
    (string-upcase name)))

If you’re thinking "Hey, that looks like a let"! You’re on the right track.

1.1.2 Presentation

Let’s take a slightly more complicated form:

(define login-form
  (form* ([username (ensure binding/email (required) (shorter-than 150))]
          [password (ensure binding/text (required) (longer-than 8))])
    (list username password)))

This form expects a valid e-mail address and a password longer than 8 characters and it returns a list containing the two values on success. To render this form to HTML, we can define a function that returns an x-expression:

(define (render-login-form)
  '(form
     ((action "")
      (method "POST"))
     (label
       "Username"
       (input ((type "email") (name "username"))))
     (label
       (input ((type "password") (name "password"))))
     (button ((type "submit")) "Login")))

This will do the trick, but it has two problems:

We can use widgets to fix both problems. First, we have to update render-login-form to take a widget-renderer/c as input:

(define (render-login-form render-widget)
  '(form
     ((action "")
      (method "POST"))
     (label
       "Username"
       (input ((type "email") (name "username"))))
     (label
       "Password"
       (input ((type "password") (name "password"))))
     (button ((type "submit")) "Login")))

Second, instead of rendering the input fields ourselves, we can tell render-widget to render the appropriate widgets for those fields:

(define (render-login-form render-widget)
  `(form
     ((action "")
      (method "POST"))
     (label
       "Username"
       ,(render-widget "username" (widget-email)))
     (label
       "Password"
       ,(render-widget "password" (widget-password)))
     (button ((type "submit")) "Login")))

Finally, we can also begin rendering errors:

(define (render-login-form render-widget)
  `(form
     ((action "")
      (method "POST"))
     (label
       "Username"
       ,(render-widget "username" (widget-email)))
     ,@(render-widget "username" (widget-errors))
     (label
       "Password"
       ,(render-widget "password" (widget-password)))
     ,@(render-widget "password" (widget-errors))
     (button ((type "submit")) "Login")))

To compose the validation and the presentation aspects, we can use form-run:

(define (make-request #:method [method #"GET"]
                      #:url [url "http://example.com"]
                      #:headers [headers null]
                      #:bindings [bindings null])
  (request method (string->url url) headers (delay bindings) #f "127.0.0.1" 8000 "127.0.0.1"))
> (form-run login-form (make-request))

'(pending #f #<procedure:...private/form.rkt:103:2>)

form-run is smart enough to figure out whether or not the request should be validated based on the request method. Because we gave it a GET request above, it returned a 'pending result and a widget renderer. That same renderer can be passed to our render-login-form function:

> (match-define (list _ _ render-widget)
    (form-run login-form (make-request)))
> (pretty-print (render-login-form render-widget))

'(form

  ((action "") (method "POST"))

  (label "Username" (input ((type "email") (name "username"))))

  (label "Password" (input ((type "password") (name "password"))))

  (button ((type "submit")) "Login"))

If we pass it an empty POST request instead, the data will be validated and a 'failed result will be returned:

> (form-run login-form (make-request #:method #"POST"))

'(failed ((username . "This field is required.") (password . "This field is required.")) #<procedure:...private/form.rkt:103:2>)

Finally, if we pass it a valid POST request, we’ll get a 'passed result:

> (form-run login-form (make-request #:method #"POST"
                                     #:bindings (list (make-binding:form #"username" #"bogdan@defn.io")
                                                      (make-binding:form #"password" #"hunter1234"))))

'(passed ("bogdan@defn.io" "hunter1234") #<procedure:...private/form.rkt:103:2>)

Putting it all together, we might write a request handler that looks like this:

(define (login req)
  (match (form-run login-form req)
    [(list 'passed (list username password) _)
     (login-user! username password)
     (redirect-to "/dashboard")]
 
    [(list _ _ render-widget)
     (response/xexpr (render-login-form render-widget))]))
1.1.3 Nested Validation

I left one thing out of the tutorial that you might be wondering about. Aside from plain values, forms can also return ok? or err? values. This makes it possible to do things like validate that two fields have the same value.

(define signup-form
  (form* ([username (ensure binding/email (required) (shorter-than 150))]
          [password (form* ([p1 (ensure binding/text (required) (longer-than 8))]
                            [p2 (ensure binding/text (required) (longer-than 8))])
                      (cond
                        [(string=? p1 p2) (ok p1)]
                        [else (err "The passwords must match.")]))])
    (list username password)))

This form will validate that the two password fields contain the same value and then return the first of them. When rendering the subform, you’d use widget-namespace to produce a widget renderer for the nested form’s fields.

1.1.4 Next Steps

If the tutorial left you wanting for more, take a look at the reference documentation below and also check out the examples folder in the source code repository.

2 Reference

2.1 Forms

struct

(struct form (constructor children)
    #:extra-constructor-name make-form)
  constructor : any/c
  children : (listof (cons/c symbol? (or (cons/c (or/c 'ok 'err) any/c) form?)))
Forms are composed of a list of formlets and other forms.

Upon successful validation, the results of each of the children are passed in order to the constructor and an ok value is returned.

On failure, an err value is returned containing a list of errors for every child that failed validation.

syntax

(form* ([name formlet] ...+)
  e ...+)
Syntactic sugar for defining forms.

procedure

(form-validate form bindings)  (cons/c (or/c 'ok 'err) any/c)

  form : form?
  bindings : (hash/c string? any/c)
Validate bindings against form.

procedure

(form-run form 
  request 
  [#:defaults defaults 
  #:submit-methods submit-methods]) 
  
(or/c
 (list/c 'passed any/c widget-renderer/c)
 (list/c 'failed any/c widget-renderer/c)
 (list/c 'pending #f widget-renderer/c))
  form : form?
  request : request?
  defaults : (hash/c string? binding?) = (hash)
  submit-methods : (listof bytes?)
   = '(#"DELETE" #"PATCH" #"POST" #"PUT")
Validate request against form.

2.2 Formlets

Formlets extract, validate and transform field values from forms.

value

binding/file : 
(-> (or/c #f binding:file?)
    (or/c (cons/c 'ok (or/c #f binding:file?))
          (cons/c 'err string?)))
Extracts an optional binding:file.

value

binding/text : 
(-> (or/c #f binding:form?)
    (or/c (cons/c 'ok (or/c #f string?))
          (cons/c 'err string?)))
Converts an optional binding:form to a string?.

value

binding/boolean : 
(-> (or/c #f binding:form?)
    (or/c (cons/c 'ok (or/c #f boolean?))
          (cons/c 'err string?)))
Converts an optional binding:form to a boolean?.

value

binding/email : 
(-> (or/c #f binding:form?)
    (or/c (cons/c 'ok (or/c #f string?))
          (cons/c 'err string?)))
Converts an optional binding:form to a string?, ensuring that it contains something vaguely resembling an e-mail address.

value

binding/number : 
(-> (or/c #f binding:form?)
    (or/c (cons/c 'ok (or/c #f number?))
          (cons/c 'err string?)))
Converts an optional binding:form to a number?.

value

binding/symbol : 
(-> (or/c #f binding:form?)
    (or/c (cons/c 'ok (or/c #f symbol?))
          (cons/c 'err string?)))
Converts an optional binding:form to a symbol?.

2.2.1 Primitives

These functions produce formlets either by combining other formlets or by "lifting" normal values into the formlet space.

procedure

(ok x)  (cons/c 'ok any/c)

  x : any/c

procedure

(ok? x)  boolean?

  x : any/c
Creates a formlet that always returns x.

procedure

(err x)  (cons/c 'err any/c)

  x : any/c

procedure

(err? x)  boolean?

  x : any/c
Creates an errored formlet.

procedure

(ensure f ...+)  
(-> any/c (or/c (cons/c 'ok any/c)
                (cons/c 'err string?)))
  f : 
(-> any/c (or/c (cons/c 'ok any/c)
                (cons/c 'err string?)))
Sequences two or more formlets together, producing a formlet that short-circuits on the first error.

2.2.2 Validators

These functions produce basic validator formlets.

procedure

(required [#:message message])

  
(-> (or/c #f any/c)
    (or/c (cons/c 'ok string?)
          (cons/c 'err string?)))
  message : string? = "This field is required."
Ensures that a non-empty string? value is present.

procedure

(matches pattern [#:message message])

  
(-> (or/c string? #f)
    (or/c (cons/c 'ok string?)
          (cons/c 'err string?)))
  pattern : regexp?
  message : string?
   = (format "This field must match the regular expression ~v." p)
Ensures that an optional string? matches the given pattern.

procedure

(one-of pairs [#:message message])

  
(-> (or/c any/c #f)
    (or/c (cons/c 'ok any/c)
          (cons/c 'err string?)))
  pairs : (listof (cons/c any/c any/c))
  message : string?
   = (format "This field must contain one of the following values: ~a" (string-join (map car pairs) ", "))
Ensures that an optional string? is equal to one of the cars of the provided list of pairs, producing the cdr of the matched pair.

procedure

(shorter-than n [#:message message])

  
(-> (or/c string? #f)
    (or/c (cons/c 'ok string?)
          (cons/c 'err string?)))
  n : exact-positive-integer?
  message : string?
   = (format "This field must contain ~a or fewer characters." (sub1 n))
Ensures that an optional string? is shorter than n.

procedure

(longer-than n [#:message message])

  
(-> (or/c string? #f)
    (or/c (cons/c 'ok string?)
          (cons/c 'err string?)))
  n : exact-positive-integer?
  message : string?
   = (format "This field must contain ~a or more characters." (add1 n))
Ensures that an optional string? is longer than n.

2.3 Widgets

Widgets render fields into xexpr?s.

procedure

(widget-input #:type type    
  [#:omit-value? omit-value?    
  #:attributes attributes])  widget/c
  type : string?
  omit-value? : boolean? = #f
  attributes : attributes/c = null
Returns a widget that can render an <input> element.

procedure

(widget-errors #:class class)  widget/c

  class : string?
Returns a widget that can render errors.

> ((widget-errors) "example" #f null)

'()

> ((widget-errors) "example" #f '((example . "this field is required")
                                  (another-field . "this field is required")))

'((ul ((class "errors")) (li "this field is required")))

procedure

(widget-checkbox [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="checkbox"> element.

> ((widget-checkbox) "example" #f null)

'(input ((type "checkbox") (name "example")))

> ((widget-checkbox) "example" (binding:form #"" #"value") null)

'(input ((type "checkbox") (name "example") (value "value") (checked "checked")))

procedure

(widget-email [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="email"> element.

> ((widget-email) "example" #f null)

'(input ((type "email") (name "example")))

> ((widget-email) "example" (binding:form #"" #"value@example.com") null)

'(input ((type "email") (name "example") (value "value@example.com")))

procedure

(widget-file [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="file"> element.

> ((widget-file) "example" #f null)

'(input ((type "file") (name "example")))

> ((widget-file) "example" (binding:file #"" #"filename" null #"content") null)

'(input ((type "file") (name "example")))

procedure

(widget-hidden [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="hidden"> element.

> ((widget-hidden) "example" #f null)

'(input ((type "hidden") (name "example")))

> ((widget-hidden) "example" (binding:form #"" #"value") null)

'(input ((type "hidden") (name "example") (value "value")))

procedure

(widget-number [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="number"> element.

> ((widget-number) "example" #f null)

'(input ((type "number") (name "example")))

> ((widget-number) "example" (binding:form #"" #"1") null)

'(input ((type "number") (name "example") (value "1")))

procedure

(widget-password [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="password"> element.

> ((widget-password) "example" #f null)

'(input ((type "password") (name "example")))

> ((widget-password) "example" (binding:form #"" #"value") null)

'(input ((type "password") (name "example")))

procedure

(widget-select options    
  [#:attributes attributes])  widget/c
  options : select-options/c
  attributes : attributes/c = null
Returns a widget that can render a <select> element.

> (define sel
    (widget-select '(("value-a" . "Label A")
                     ("Countries" (("romania" . "Romania")
                                   ("usa" . "United States of America")))
                     ("Languages" (("english" . "English")
                                   ("racket" . "Racket"))))))
> (pretty-print (sel "example" #f null))

'(select

  ((name "example"))

  (option ((value "value-a")) "Label A")

  (optgroup

   ((label "Countries"))

   (option ((value "romania")) "Romania")

   (option ((value "usa")) "United States of America"))

  (optgroup

   ((label "Languages"))

   (option ((value "english")) "English")

   (option ((value "racket")) "Racket")))

> (pretty-print (sel "example" (binding:form #"" #"racket") null))

'(select

  ((name "example"))

  (option ((value "value-a")) "Label A")

  (optgroup

   ((label "Countries"))

   (option ((value "romania")) "Romania")

   (option ((value "usa")) "United States of America"))

  (optgroup

   ((label "Languages"))

   (option ((value "english")) "English")

   (option ((value "racket") (selected "selected")) "Racket")))

procedure

(widget-radio-group options    
  [#:attributes attributes])  widget/c
  options : radio-options/c
  attributes : attributes/c = null
Returns a widget that can render a group of <input type="radio"> elements.

> (define rg
   (widget-radio-group '(("value-a" . "Label A")
                         ("value-b" . "Label B"))))
> (pretty-print (rg "example" #f null))

'(div

  (label (input ((type "radio") (name "example") (value "value-a"))) "Label A")

  (label

   (input ((type "radio") (name "example") (value "value-b")))

   "Label B"))

> (pretty-print (rg "example" (binding:form #"" #"value-a") null))

'(div

  (label

   (input

    ((type "radio") (name "example") (value "value-a") (checked "checked")))

   "Label A")

  (label

   (input ((type "radio") (name "example") (value "value-b")))

   "Label B"))

procedure

(widget-text [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render an <input type="text"> element.

> ((widget-text) "example" #f null)

'(input ((type "text") (name "example")))

> ((widget-text) "example" (binding:form #"" #"value") null)

'(input ((type "text") (name "example") (value "value")))

procedure

(widget-textarea [#:attributes attributes])  widget/c

  attributes : attributes/c = null
Returns a widget that can render a <textarea> element.

> ((widget-textarea) "example" #f null)

'(textarea ((name "example")))

> ((widget-textarea) "example" (binding:form #"" #"value") null)

'(textarea ((name "example")) "value")

procedure

(widget-namespace namespace    
  widget-renderer)  widget/c
  namespace : string?
  widget-renderer : widget-renderer/c
Returns a widget renderer for the subform whose id is namespace.

2.3.1 Contracts

The contract for element attributes.

The contract for lists of validation errors.

The contract for a list of <input type="radio"> options.

The contract for a list of <select> element options. The list-based variant is used to represent option groups (<optgroup> elements).

The contract for widgets. Widgets take a field name, its associated binding (if any) and a list of errors and produces either one or a list of xexpr?s.

The contract for widget renderers. Renderers take the name of a field and a widget that will render the field as one or more xexpressions.