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
1.2 Limitations
2 Reference
2.1 Formlets
binding/  file
binding/  text
binding/  boolean
binding/  email
binding/  number
binding/  symbol
2.1.1 Primitives
ok
ok?
err
err?
ensure
2.1.2 Validators
required
matches
one-of
shorter-than
longer-than
2.2 Forms
form
form*
form-validate
form-run
2.3 Widgets
attributes/  c
errors/  c
options/  c
widget/  c
widget-renderer/  c
widget-namespace
widget-errors
widget-input
widget-checkbox
widget-email
widget-file
widget-hidden
widget-number
widget-password
widget-select
widget-radio-group
widget-text
widget-textarea
7.3

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 (and potentially json, xml and other structured 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, functions that can take an input, validate it and potentially transform it into another value.

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 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 rendering function 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:...rivate/forms.rkt:101:2>)

form-run is smart enough to figure out whether or not the request should be validated based on the request method. Because we passed it a (fake) 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:...rivate/forms.rkt:101: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:...rivate/forms.rkt:101:2>)

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

(define (login req)
  (match (form-run 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 value.

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.

1.2 Limitations

The following features are not currently supported (and may never be):

2 Reference

2.1 Formlets

value

binding/file

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

value

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

value

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

value

binding/email : 
(-> (or/c false/c binding:form?)
    (or/c (cons/c 'ok (or/c false/c 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 false/c binding:form?)
    (or/c (cons/c 'ok (or/c false/c number?))
          (cons/c 'err string?)))
Converts an optional binding:form to a number?.

value

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

2.1.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
Create a formlet that always returns x.

procedure

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

  x : any/c

procedure

(err? x)  boolean?

  x : any/c
Create 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?)))
Sequence two or more formlets together, producing a formlet that short-circuits on the first error.

2.1.2 Validators

These functions produce basic validator formlets.

procedure

(required [#:message message])

  
(-> (or/c false/c 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 (false/c string?))
    (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 (false/c any/c))
    (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 (false/c string?))
    (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 (false/c string?))
    (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.2 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?)))
A form that can be used to validate children together and produce a result value by passing the results of each field to constructor.

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 false/c 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.3 Widgets

value

attributes/c : (listof (list/c symbol? string?))

value

errors/c

 : 
(listof
  (or/c string? (cons/c symbol? (or/c string? errors/c))))

value

options/c : (listof (cons/c string? string?))

value

widget/c

 : (-> string? (or/c false/c binding?) errors/c (or/c xexpr/c (listof xexpr/c)))

value

widget-renderer/c

 : (-> string? widget/c (or/c xexpr/c (listof xexpr/c)))
Various widget-related contracts.

procedure

(widget-namespace namespace    
  widget-renderer)  widget/c
  namespace : string?
  widget-renderer : widget-renderer/c
Produce a widget renderer for a subform.

procedure

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

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

procedure

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

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a checkbox INPUT element.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a email INPUT element.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a file INPUT element.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a hidden INPUT element.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a number INPUT element.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a password INPUT element.

procedure

(widget-select options    
  [#:attributes attributes])  widget/c
  options : (or/c (hash/c string? options/c) options/c)
  attributes : attributes/c = null
Produce a widget that can render a SELECT element.

procedure

(widget-radio-group options    
  [#:attributes attributes])  widget/c
  options : options/c
  attributes : attributes/c = null
Produce a widget that can render a group of radio INPUTS.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a text INPUT element.

procedure

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

  attributes : attributes/c = null
Produce a widget that can render a textarea INPUT element.