Commits

Steve Losh  committed e61d618

Moar changes.

  • Participants
  • Parent commits a58c791

Comments (0)

Files changed (17)

File docs/01-installation.markdown

 
 That's all there is to it.  Red Tape depends on one other library (Slingshot)
 but Leiningen should handle that for you.
+
+Once you've got it installed, move on to the [basics guide](../basics/).

File docs/02-basics.markdown

 (strings), validate it, and turn it into useful data structures.
 
 Red Tape does *not* handle rendering form fields into HTML.  That's the job of
-your templating library, and you always needs to customize `<input>` tags
-anyway.
+your templating library, and you always need to customize `<input>` tags anyway.
 
 It's designed with Ring, Compojure, and friends in mind (though it's not limited
 to them) so let's take a look at a really simple application to see it in
     (ns feedback
       (:require [compojure.core :refer :all]
                 [compojure.route :as route]
+                [hiccup.page :refer [html5]]
                 [ring.adapter.jetty :refer [run-jetty]]
                 [ring.middleware.params :refer [wrap-params]]))
 
-    (def page "
-        <html>
-            <body>
-                <label>Who are you?</label>
-                <input type='text' name='name'/>
-                <label>What do you want to say?</label>
-                <textarea name='comment'/>
-            </body>
-        </html>
-    ")
+    (defn page []
+      (html5
+        [:body
+         [:form {:method "POST" :action "/"}
+          [:label "Who are you?"]
+          [:input {:type "text" :name "name"}]
+          [:label "What do you want to say?"]
+          [:textarea {:name "comment"}]]]))
 
     (defn save-feedback [from comment]
       ; In a real app this would save the string to a database, email
       ([request]
         (handle-get request (feedback-form)))
       ([request form]
-        page))
+        (page)))
 
 There are a couple of things going on here.
 
 the second piece, which actually renders the page.  You'll see why we split it
 up like that shortly.
 
-Calling `(feedback-form)` without data returns a map representing a fresh form.
-It will look like this:
+Calling `(feedback-form)` without data returns a result map representing a fresh
+form.  It will look like this:
 
     :::clojure
     {:fresh true
      :errors nil}
 
 In a nutshell, this is all Red Tape does.  You define form functions using
-`defform`, and those functions take in data and turn it into a map like this.
+`defform`, and those functions take in data and turn it into a result map like
+this.
 
 Let's add a bit of data cleaning to the form to get something more useful.
 
 Let's look at an example:
 
     :::clojure
-    (defform age-form
+    (defform age-form {}
       :age [clojure.string/trim
             #(Long. %)])
 
 Finally, the `:data` entry in the result map is present and contains the data
 the user entered, even though it turned out to be invalid.
 
+Built-In Cleaners
+-----------------
+
+Red Tape contains a number of useful cleaner functions pre-defined in the
+`red-tape.cleaners` namespace.
+
+We'll use `red-tape.cleaners/non-blank` in this tutorial.  `non-blank` is
+a simple cleaner that throws an exception if it receives an empty string, or
+otherwise passes through the data unchanged.
+
+Let's change the form to make sure that users don't try to submit an empty
+comment (but we'll still allow an empty name, in case someone wants to comment
+anonymously):
+
+    :::clojure
+    (ns feedback
+      ; ...
+      (require [red-tape.cleaners :as cleaners]))
+
+    (defform feedback-form {}
+      :name [clojure.string/trim]
+      :comment [clojure.string/trim cleaners/non-blank])
+
+Notice that we trim whitespace *before* checking for a non-blank string, so
+a comment of all whitespace would result in an error.
+
 Putting it All Together
 -----------------------
 
 Now that we've seen how to clean and validate, we can finally connect the
 missing pieces to our feedback form.
 
-First we'll redefine our little HTML page so we can include some initial data in
-it:
+First we'll redefine our little HTML page to take the form as an argument, so we
+can pre-fill the inputs with any initial data:
 
     :::clojure
-    (def page "
-        <html>
-            <body>
-                <label>Who are you?</label>
-                <input type='text' name='name' value='%s'/>
-                <label>What do you want to say?</label>
-                <textarea name='comment' value='%s'/>
-            </body>
-        </html>
-    ")
+    (defn page [form]
+      (html5
+        [:body
+         [:form {:method "POST" :action "/"}
+          [:label "Who are you?"]
+          [:input {:type "text" :name "name"
+                   :value (get-in form [:data :name])}]
+          [:label "What do you want to say?"]
+          [:textarea {:name "comment"} (get-in form [:data :comment])]]]))
 
-The only change here is the `value='%s'` bits, which we'll use to stick in some
-data later.
+Notice how we pull the values of each field out of the `:data` entry in the form
+result map.
 
-We'll redefine `feedback-form` one last time:
-
-    :::clojure
-    (defform feedback-form {}
-      :name [clojure.string/trim]
-      :comment [clojure.string/trim])
-
-Now we can write the GET handler:
+Now we can write the final GET handler:
 
     :::clojure
     (defn handle-get
       ([request]
         (handle-get request (feedback-form)))
       ([request form]
-        (let [initial-name (:name (:data form))
-              initial-comment (:comment (:data form))]
-          (format page initial-name initial-comment))))
+        (page form)))
 
-Notice how we use the `:data` from the form when we're rendering the page.  This
-will make more sense once you see the POST handler:
+And the POST handler:
 
     :::clojure
     (defn handle-post [request]
 
 If it's *not* valid, we use the GET handler to re-render the form without
 redirecting.  We pass along our *invalid* form as we do that, so that when the
-GET handler uses the `:data` it will fill in the fields correctly so the user
-doesn't have to retype everything.
+GET handler calls the `page` and uses the `:data` it will fill in the fields
+correctly so the user doesn't have to retype everything.
 
 Summary
 -------
   and does the appropriate thing depending on whether it's valid or not.
 
 Now that you've got the general idea, it's time to look at a few topics in more
-detail.
+detail.  Start with the [form input](../input/) guide.
+

File docs/03-cleaners.markdown

-Cleaners
-========
-
-Cleaners are the workhorses of Red Tape.  They massage your form's data into the
-shape you want, and detect bad data so you can bail out if necessary.
-
-[TOC]
-
-Cleaners are Functions
-----------------------
-
-Cleaners are plain old Clojure functions -- there's nothing special about them.
-They take one argument (the data to clean) and return a result.
-
-Here are a few examples:
-
-    :::clojure
-    ; A cleaner to turn the raw string into a Long.
-    (defn to-long [v]
-      (Long. v))
-
-    ; A cleaner to take a Long user ID and look up the
-    ; user in a database.
-    (def users {1 "Steve"})
-
-    (defn to-user [id]
-      (let [user (get users id)]
-        (if user
-          user
-          (throw+ "Invalid user ID!"))))
-
-    ; Use both of these cleaners to first turn the input
-    ; string into a long, then into a user.
-    (defform user-form {}
-      :user [to-long to-user])
-
-    ; Now we'll call the form with some data.
-    (user-form {"user" "1"})
-    ; =>
-    {:valid true
-     :results {:user "Steve"}
-     ...}
-
-Validation Errors
------------------
-
-Cleaners can report a validation error by throwing an exception, or by using
-Slingshot's `throw+` to throw *anything*.
-
-If a cleaner throws something, the result map's `:valid` entry will be `false`
-and the `:errors` entry will contain whatever was thrown.  Continuing the
-example above:
-
-    :::clojure
-    (user-form {"user" "400"})
-    ; =>
-    {:valid false
-     :errors {:user "Invalid user ID!"}
-     ...}
-
-Form-Level Cleaners
--------------------
-
-Built-In Cleaners
------------------
-
-Red Tape contains a number of common cleaners in `red-tape.cleaners`.  See the
-[Reference](../reference) section for the full list.
-

File docs/03-input.markdown

+Form Input
+==========
+
+Red Tape forms take a map of data as input.  Red Tape is designed with web-based
+forms in mind, but since it operates on plain old maps you could theoretically
+use it for other things as well.
+
+[TOC]
+
+Keys
+----
+
+The input map should contain keys that match the field definitions of a form.
+For example:
+
+    :::clojure
+    (defform change-password-form {}
+      :old-password [...]
+      :new-password-1 [...]
+      :new-password-2 [...])
+
+    (change-password-form {:old-password "foo"
+                           :new-password-1 "bar"
+                           :new-password-2 "bar"})
+    ; =>
+    {... result map ...}
+
+For convenience, the keys in the input map can be strings and everything will
+still work:
+
+    :::clojure
+    (change-password-form {"old-password" "foo"
+                           "new-password-1" "bar"
+                           "new-password-2" "bar"})
+    ; =>
+    {... result map ...}
+
+If the input map contains both a string and a keyword key for a field, the
+results are undefined.  Don't do that.
+
+Values
+------
+
+All values in the input map should be strings.
+
+Using other types of data may *appear* to work, but Red Tape's behavior may
+change in the future so you really should just stick to string input and use
+cleaners to convert them to whatever kind of data you want.
+
+Move on to the [cleaners](../cleaners/) guide to learn more about this.

File docs/04-cleaners.markdown

+Cleaners
+========
+
+Cleaners are the workhorses of Red Tape.  They massage your form's data into the
+shape you want, and detect bad data so you can bail out if necessary.
+
+[TOC]
+
+Cleaners are Functions
+----------------------
+
+Cleaners are plain old Clojure functions -- there's nothing special about them.
+They take one argument (the data to clean) and return a result.
+
+Let's look at a few examples.  First we have a cleaner function that takes
+a value and turns it into a Long:
+
+    :::clojure
+    ; A cleaner to turn the raw string into a Long.
+    (defn to-long [v]
+      (Long. v))
+
+The next cleaner function takes a user ID, looks up the user in a "database",
+and returns the user.  We'll talk about the `throw+` in the next section.
+
+    :::clojure
+    ; A cleaner to take a Long user ID and look up the
+    ; user in a database.
+    (def users {1 "Steve"})
+
+    (defn to-user [id]
+      (let [user (get users id)]
+        (if user
+          user
+          (throw+ "Invalid user ID!"))))
+
+Now we can use these two cleaners in a simple form:
+
+    :::clojure
+    ; Use both of these cleaners to first turn the input
+    ; string into a long, then into a user.
+    (defform user-form {}
+      :user [to-long to-user])
+
+    ; Now we'll call the form with some data.
+    (user-form {"user" "1"})
+    ; =>
+    {:valid true
+     :results {:user "Steve"}
+     ...}
+
+Validation Errors
+-----------------
+
+Cleaners can report a validation error by throwing an exception, or by using
+Slingshot's `throw+` to throw *anything*.
+
+If a cleaner throws something, the result map's `:valid` entry will be `false`
+and the `:errors` entry will contain whatever was thrown.  Continuing the
+example above:
+
+    :::clojure
+    (user-form {"user" "400"})
+    ; =>
+    {:valid false
+     :errors {:user "Invalid user ID!"}
+     ...}
+
+What happened here?
+
+First, the string `"400"` was given to the first cleaner, which turned it into
+the long `400`.
+
+Then that long was given to the second cleaner, which tried to look it up in the
+database.  Since it wasn't found, the cleaner used `throw+` to throw a string as
+an error, so the form was marked as invalid.
+
+Form-Level Cleaners
+-------------------
+
+Sometimes you need to clean or validate based on more than one field in your
+form.  For that you need to use form-level cleaners.
+
+Form-level cleaners are similar to field cleaners: they're vanilla Clojure
+functions that take and return a single value.  That value is a map of all the
+fields, *after* the field-level cleaners have been run.
+
+Note that if any individual fields had errors, the form-level cleaners will
+*not* be run.  It doesn't make sense to run them on garbage input.
+
+They can throw errors just like field-level cleaners too.
+
+Let's look at how to use form-level cleaners with a simple example:
+
+    :::clojure
+    (defn new-passwords-match [form-data]
+      (if (not= (:new-password-1 form-data)
+                (:new-password-2 form-data))
+        (throw+ "New passwords do not match!")
+        form-data))
+
+    (defform change-password-form {}
+      :old-password []
+      :new-password-1 []
+      :new-password-2 []
+      :red-tape/form new-passwords-match)
+
+    (change-password-form {:old-password "foo"
+                           :new-password-1 "a"
+                           :new-password-2 "b"})
+    ; =>
+    {:valid false
+     :errors {:red-tape/form "New passwords do not match!"}}
+
+There's a lot to see here.  First, we defined a function that takes a map of
+form data (after any field cleaners have been run).
+
+If the new password fields match, the function returns the map of data.  In this
+case it doesn't modify it at all, but we could if we wanted to.
+
+If the new passwords don't match, an error is thrown with Slingshot's `throw+`.
+
+Next we define the form.  The form-level cleaners are specified by attaching
+them to the special `:red-tape/form` "field".
+
+Notice how the form-level cleaner in the example is given on its own, not as
+a vector.  There are actually three ways to specify form-level cleaners,
+depending on how they need to interact.
+
+The first way is to give a single function like we did in the example:
+
+    :::clojure
+    (defform foo {}
+      ...
+      :red-tape/form my-cleaner)
+
+If you only have one form-level cleaner this is the simplest way to go.
+
+The second option is to give a vector of functions, just like field cleaners:
+
+    :::clojure
+    (defform foo {}
+      ...
+      :red-tape/form [my-cleaner-1 my-cleaner-2])
+
+These will be run in sequence, with the output of each feeding into the next.
+This allows you to split up your form-level cleaners just like your field-level
+ones.
+
+Finally, you can give a set containing zero or more entries of either of the
+first two types: 
+
+    :::clojure
+    (defform foo {}
+      ...
+      :red-tape/form #{my-standalone-cleaner
+                       [my-cleaner-part-1
+                        my-cleaner-part-2]})
+
+Each entry in the set will be evaluated according to the rules above, and its
+output fed into the other entries.
+
+This happens in an unspecified order, so you should only use a set to define
+form-level cleaners that explicitly do *not* depend on each other.  If one
+cleaner depends on another one adjusting the data first, you need to use
+a vector to make sure they run in the correct order.
+
+Built-In Cleaners
+-----------------
+
+Red Tape contains a number of common cleaners in `red-tape.cleaners`.  See the
+[Reference](../reference) section for the full list.
+
+Results
+-------
+
+Once all cleaners have been run on the data, the results (or errors) will be
+returned as a result map.  Read the [result maps](../result-maps/) guide for
+more information.
+

File docs/04-result-maps.markdown

-Result Maps
-===========
-
-After you pass data through a form you'll get a result map back.  This map
-contains a number of entries that describe the results of running the cleaners
-on that data (or some placeholders when you run a form without data).  Let's
-look at each of the entries in detail.
-
-[TOC]
-
-:fresh
-------
-
-This will be `true` if this is a fresh form (i.e.: it was called without data),
-and `false` otherwise.
-
-:valid
-------
-
-If the form was called with data, and the data passed through all cleaners
-without anything being thrown, this will be `true`.  Otherwise it will be
-`false.`
-
-This should be used to determine whether to trust the data from the form, or
-whether to re-render it for the user (and display the errors).
-
-:errors
--------
-
-If the form was called with data and a cleaner threw an exception (or anything
-else with Slingshot's `throw+`) then `:errors` will be a map of field keys to
-whatever was thrown.  For example:
-
-    :::clojure
-
-    (defn always-fail [x]
-      (throw+ "Error!"))
-
-    (defform failing-form {}
-      :foo [always-fail]
-      :bar [])
-
-    (failing-form {"foo" "aaa"
-                   "bar" "bbb"})
-    ; =>
-    {:errors {:foo "Error!"}
-     ...}
-
-Only fields that actually threw errors will have entries in the `:errors` map.
-
-If no fields threw an error, `:errors` will be `nil`.
-
-:data
------
-
-The `:data` entry in the result map will contain the raw data that was passed
-into the form, *before* it was run through the cleaners.  This is useful for
-re-rendering the form when there are errors.
-
-TODO: More.
-
-:results
---------
-
-The `:results` entry is a map of field keys to the values *after* they've been
-passed through all the cleaners.  After you've checked that the form is valid by
-looking at `:valid`, you should use the data in `:results` to do whatever you
-need to do.
-
-If this is a fresh form, or if any cleaners threw errors, `:results` will be
-`nil`.
-
-:arguments
-----------
-
-The `:arguments` entry is a mapping of keywords to form arguments, which will be
-discussed in a later chapter.
-

File docs/05-initial-data.markdown

-Initial Data
-============
-
-Sometimes you want to provide sane defaults for fields in a form.  To do this
-you can use the `:initial` entry in the `defform` option map.  For example:
-
-    :::clojure
-    (defform sample-form {:initial {:email "user@example.com"}}
-      :email [])
-
-When creating a fresh form, this initial data will populate the `:data` map in
-the result:
-
-    :::clojure
-    (sample-form)
-    ; =>
-    {:fresh true
-     :results nil
-     :data {:email "user@example.com"}
-     ...}
-
-When you run some data through the form, the initial data is ignored:
-
-    :::clojure
-    (sample-form {"email" "foo@bar.com"})
-    ; =>
-    {:fresh false
-     :results {:email "foo@bar.com"}
-     :data    {:email "foo@bar.com"}
-     ...}
-
-This means when you're rendering a form field to HTML you can simply pull the
-field's entry from the `:data` map and fill the field with its contents.  It
-doesn't matter whether it happed to be a fresh form or a form that had some
-errors.
-
-Note that initial data should always be specified as a string, since the `:data`
-map will contain strings once you run data through it.
-

File docs/05-result-maps.markdown

+Result Maps
+===========
+
+After you pass data through a form (or call a form without data) you'll get
+a result map back.
+
+This map contains a number of entries that describe the results of running the
+cleaners on that data (or some placeholders when you run a form without data).
+Let's look at each of the entries in detail.
+
+Once you're done here, move on to the [initial data](../initial-data/) guide.
+
+[TOC]
+
+:fresh
+------
+
+This will be `true` if this is a fresh form (i.e.: it was called without data),
+and `false` otherwise.
+
+:valid
+------
+
+If the form was called without data, this will be `nil`.
+
+Otherwise, if the form was called with data, and the data passed through all
+cleaners without anything being thrown, this will be `true`.
+
+Otherwise it will be `false.`
+
+This should be used to determine whether to trust the data from the form, or
+whether to re-render it for the user (and display the errors).
+
+:errors
+-------
+
+If the form was called with data and a cleaner threw an exception (or anything
+else with Slingshot's `throw+`) then `:errors` will be a map of field keys to
+whatever was thrown.  For example:
+
+    :::clojure
+
+    (defn always-fail [x]
+      (throw+ "Error!"))
+
+    (defform failing-form {}
+      :foo [always-fail]
+      :bar [])
+
+    (failing-form {"foo" "aaa"
+                   "bar" "bbb"})
+    ; =>
+    {:errors {:foo "Error!"}
+     ...}
+
+Only fields that actually threw errors will have entries in the `:errors` map.
+
+If any form-level cleaners threw an error, `:errors` will contain an entry for
+`:red-tape/form`.  This will be a vector of form-level errors.
+
+TODO: Explain why it's a vector (when a set is given).
+
+If errors were thrown anywhere, `:errors` will be `nil`.
+
+:data
+-----
+
+The `:data` entry in the result map will contain the raw data that was passed
+into the form, *before* it was run through the cleaners.
+
+This is useful for re-rendering the form when there are errors.
+
+TODO: give a full example here.
+
+If this is the result map of a fresh form (a form that has been called without
+form data), every entry in `:data` will be an empty string (or initial data,
+which we'll discuss in the next chapter).
+
+:results
+--------
+
+The `:results` entry is a map of field keys to the values *after* they've been
+passed through all the cleaners.  After you've checked that the form is valid by
+looking at `:valid`, you should use the data in `:results` to do whatever you
+need to do.
+
+If this is a fresh form, or if any cleaners threw errors, `:results` will be
+`nil`.
+
+:arguments
+----------
+
+The `:arguments` entry is a mapping of keywords to form arguments, which will be
+discussed in a later chapter.
+

File docs/06-form-arguments.markdown

-Form Arguments
-==============
-
-Sometimes a simple form isn't enough.  When you need dynamic functionality in
-your forms you can use Red Tape's form arguments.
-
-[TOC]
-
-The Problem
------------
-
-Let's use a simple example to demonstrate why form arguments are necessary and
-how they work.  Suppose you have a site that hosts videos.  You'd like to have
-a "Delete Video" form to let users delete their videos.  A first crack at this
-might look like this:
-
-    :::clojure
-    (defn video-exists [video-id]
-      ; Checks that the given video id exists...
-    )
-
-    (defform delete-video {}
-      :video-id [video-exists])
-
-Nothing new here.  But this form will let *anyone* delete *any* video.  In real
-life you probably only want users to be able to delete their *own* videos.
-
-Defining Form Arguments
------------------------
-
-To define form arguments you can use the `:arguments` entry in the form options
-map, passing a vector of symbols.  Let's change our `delete-video` form:
-
-    :::clojure
-    (defform delete-video {:arguments [user]}
-      :video-id [video-exists])
-
-The form will now take a `user` argument.  So calling the form without data
-would be done like this:
-
-    :::clojure
-    (let [current-user ...
-          fresh-delete-form (delete-form current-user)])
-
-And calling it with data would look like this:
-
-    :::clojure
-    (let [current-user ...
-          post-data ...
-          form (delete-form current-user post-data)])
-
-Using Form Arguments
---------------------
-
-Using form arguments is simple.  There's one key idea: your cleaner definitions
-are evaluated in a context where the form arguments are bound.
-
-Here's how we could use our `user` argument:
-
-    :::clojure
-    (defn owned-by [user video-id]
-      ; Check that the given user owns the given video id.
-    )
-
-    (defform delete-video {:arguments [user]}
-      :video-id [video-exists (partial owned-by user)])
-
-    (let [current-user ...
-          post-data ...
-          form (delete-form current-user post-data)])
-
-Notice how the second cleaner in the `defform` is defined as `(partial owned-by
-user)`.  `user` here is the form argument, so the partial function will be
-created with the appropriate user each time `delete-form` is called.
-
-Initial Data
-------------
-
-The initial data map is also evaluated in a context where the form arguments are
-bound.
-
-TODO: More about this.

File docs/06-initial-data.markdown

+Initial Data
+============
+
+Sometimes you want to provide sane defaults for fields in a form.  To do this
+you can use the `:initial` entry in the `defform` option map.  For example:
+
+    :::clojure
+    (defform sample-form {:initial {:email "user@example.com"}}
+      :email []
+      :message [])
+
+When creating a fresh form, this initial data will populate the `:data` map in
+the result instead of the default empty string:
+
+    :::clojure
+    (sample-form)
+    ; =>
+    {:fresh true
+     :results nil
+     :data {:email "user@example.com"
+            :message ""}
+     ...}
+
+When you run some data through the form, the initial data is ignored:
+
+    :::clojure
+    (sample-form {:email "foo@bar.com"
+                  :message "hi"})
+    ; =>
+    {:fresh false
+     :results {:email "foo@bar.com"
+               :message "hi"}
+     :data    {:email "foo@bar.com"
+               :message "hi"}
+     ...}
+
+This means when you're rendering a form field to HTML you can simply pull the
+field's entry from the `:data` map and fill the field with its contents.  It
+doesn't matter whether it happened to be a fresh form or a form that had some
+errors.
+
+Note that initial data should always be specified as a string, since the `:data`
+map will always contain strings once you've run data through it.
+
+Move on to the [form arguments](../form-arguments/) guide next.

File docs/07-form-arguments.markdown

+Form Arguments
+==============
+
+Sometimes a simple form isn't enough.  When you need dynamic functionality in
+your forms you can use Red Tape's form arguments.
+
+Once you've gone through this document, read the [rendering](../rendering/)
+guide.
+
+[TOC]
+
+The Problem
+-----------
+
+Let's use a simple example to demonstrate why form arguments are necessary and
+how they work.  Suppose you have a site that hosts videos.  You'd like to have
+a "Delete Video" form to let users delete their videos.  A first crack at this
+might look like this:
+
+    :::clojure
+    (defn video-exists [video-id]
+      ; Checks that the given video id exists...
+    )
+
+    (defform delete-video-form {}
+      :video-id [video-exists])
+
+Nothing new here.  But this form will let *anyone* delete *any* video.  In real
+life you probably only want users to be able to delete their *own* videos.
+
+Defining Form Arguments
+-----------------------
+
+To define form arguments you can use the `:arguments` entry in the form options
+map, passing a vector of symbols.  Let's change our `delete-video-form` form:
+
+    :::clojure
+    (defform delete-video-form {:arguments [user]}
+      :video-id [video-exists])
+
+The form will now take a `user` argument.  So calling the form without data
+would be done like this:
+
+    :::clojure
+    (let [current-user ...
+          fresh-delete-form (delete-video-form current-user)])
+
+And calling it with data would look like this:
+
+    :::clojure
+    (let [current-user ...
+          post-data ...
+          form (delete-video-form current-user post-data)])
+
+Using Form Arguments
+--------------------
+
+Using form arguments is simple.  There's one key idea: your cleaner definitions
+are evaluated in a context where the form arguments are bound.
+
+Here's how we could use our `user` argument:
+
+    :::clojure
+    (defn owned-by [user video-id]
+      ; Check that the given user owns the given video id.
+    )
+
+    (defform delete-video-form {:arguments [user]}
+      :video-id [video-exists (partial owned-by user)])
+
+    (let [current-user ...
+          post-data ...
+          form (delete-video-form current-user post-data)])
+
+Notice how the second cleaner in the `defform` is defined as `(partial owned-by
+user)`.  `user` here is the form argument, so the partial function will be
+created with the appropriate user each time `delete-form` is called.
+
+Initial Data
+------------
+
+The initial data map is also evaluated in a context where the form arguments are
+bound.
+
+TODO: More about this.

File docs/07-reference.markdown

-Reference
-=========

File docs/08-rendering.markdown

+Rendering
+=========
+
+Red Tape does not provide any built-in functionality for rendering HTML.  It
+leaves that up to you, so you can choose your preferred templating library.
+However, it's built with a few key ideas in mind that should make it easier for
+you do create forms that are usable and friendly for your users.
+
+Let's look at a full example that shows all the things you should think about
+when rendering a form.  This example will use the Hiccup templating library for
+brevity, but as mentioned you can use any templating library you like.
+
+[TOC]
+
+Basic Structure
+---------------
+
+Recall the structure of the GET handler from the [basics](../basics/) guide:
+
+    :::clojure
+    (defn handle-get
+      ([request]
+        (handle-get request (your-form)))
+      ([request form]
+        (... render the page, passing in the form ...)))
+
+And also recall the POST handler:
+
+    :::clojure
+    (defn handle-post [request]
+      (let [data (:params request)
+            form (your-form data)]
+        (if (:valid form)
+          (... do whatever you need, then redirect somewhere else ...)
+          (handle-get request form))))
+
+As you can see, there's only one place where the form gets rendered in this
+code.  This means your rendering code will all be in a single place, and should
+handle result maps from fresh forms as well as ones that contain errors.
+
+Rendering a Fresh Form
+----------------------
+
+Let's talk about the fresh form first.  We'll use a simple form as an example:
+
+    :::clojure
+    (require '[red-tape.cleaners])
+
+    (defform simple-form {:initial {:name "Steve"}}
+      :name [red-tape.cleaners/non-blank])
+
+Now we'll create the get handler for this:
+
+    :::clojure
+    (defn handle-get
+      ([request]
+        (handle-get request (simple-form)))
+      ([request form]
+        (simple-page form)))
+
+Now we'll use Hiccup to create `simple-page`:
+
+    :::clojure
+    (defpage simple-page [form]
+      (html
+        [:body
+         [:form {:method "POST"}
+          [:label "What is your name?"]
+          [:input {:name "name"
+                   :value (get-in form [:data :name])}]
+          [:input {:type "submit"}]]]))
+
+Notice how the `value` for the name input is pulled out of the `:data` entry of
+the result map.  This ensures that the field will be filled in with the correct
+initial data ("Steve" in this example).
+
+Rendering Errors
+----------------
+
+Suppose the user backspaces out the initial name and submits the form.  The
+`non-blank` cleaner in the form means that a blank name is considered invalid.
+
+The usual POST handler for a Red Tape form will use the GET handler to re-render
+the result map of the invalid form, so our template should take this into
+account and display errors properly.  Let's see how we might do that:
+
+    :::clojure
+    (defpage simple-page [form]
+      (html
+        [:body
+         (if (:errors form)
+           [:p "There was a problem.  Please try again."])
+         [:form {:method "POST"}
+          [:label "What is your name?"]
+          (when-let [error (get-in form [:errors :name])]
+            [:p error])
+          [:input {:name "name"
+                   :value (get-in form [:data :name])}]
+          [:input {:type "submit"}]]]))
+
+There are two additions here.  First we check if there were any errors at the
+top of the form.  If there were, we display a message.
+
+We also check if there were any errors for the specific field, and if so we
+display it.  In this simple example there's only one field, but in most forms
+you'll have many fields, each of which may or may not have errors.

File docs/09-reference.markdown

+Reference
+=========

File src/red_tape/cleaners.clj

 (ns red-tape.cleaners
   (:require [slingshot.slingshot :refer [try+ throw+]]))
 
-(defmacro ensure-is [value f msg]
+
+; Ensure Macros
+(defmacro ensure-is
+  "Ensure the given value returns true for the given predicate.
+
+  If the value satisfies the predicate, it is returned unchanged.
+
+  Otherwise, the given message is thrown with Slingshot's throw+.
+
+  "
+  [value pred msg]
   `(let [v# ~value]
-     (if-not (~f v#)
+     (if-not (~pred v#)
        (throw+ ~msg)
        v#)))
 
-(defmacro ensure-not [value f msg]
+(defmacro ensure-not
+  "Ensure the given value returns false for the given predicate.
+
+  If (predicate value) returns false, the value is returned unchanged.
+
+  Otherwise, the given message is thrown with Slingshot's throw+.
+
+  "
+  [value pred msg]
   `(let [v# ~value]
-     (if (~f v#)
+     (if (~pred v#)
        (throw+ ~msg)
        v#)))
 
 
-(defn non-blank [s]
+; Strings
+(defn non-blank
+  "Ensure that the string is not empty.
+
+  Whitespace is treated like any other character -- use clojure.string/trim to
+  remove it if you want.
+
+  "
+  [s]
   (ensure-not s #(= "" %1)
               "This field is required."))
 
-(defn to-long [s]
-  (Long. s))
+(defn min-length
+  "Ensure that the string is at least N characters long."
+  [n s]
+  (ensure-not s #(< (count %) n)
+              (str "This field must be at least " n " characters.")))
+
+(defn max-length
+  "Ensure that the string is at most N characters long."
+  [n s]
+  (ensure-not s #(> (count %) n)
+              (str "This field must be at most " n " characters.")))
+
+(defn length
+  "Ensure that the string is at least min and at most max characters long."
+  [min max s]
+  (->> s
+    (min-length min)
+    (max-length max)))
+
+(defn empty-to-nil
+  "Converts empty strings to nil, leaving other strings alone.
+
+  Whitespace is treated like any other character -- use clojure.string/trim to
+  remove it if you want.
+
+  "
+  [s]
+  (if (= s "")
+    nil
+    s))
+
+
+; Numerics
+(defn to-long
+  "Convert a string to a Long."
+  [s]
+  (try+
+    (Long. s)
+    (catch Object _ "Please enter a whole number.")))
+
+(defn to-double
+  "Convert a string to a Double."
+  [s]
+  (try+
+    (Double. s)
+    (catch Object _ "Please enter a number.")))
 
 (defn positive [n]
   (ensure-is n pos?
   (ensure-is n neg?
              "Please enter a negative number."))
 
-(defn min-length [n s]
-  (ensure-not s #(< (count %) n)
-              (str "This field must be at least " n " characters.")))
 
-(defn max-length [n s]
-  (ensure-not s #(> (count %) n)
-              (str "This field must be at most " n " characters.")))
+; Other
 (defn choices [cs v]
   (ensure-is v #(contains? cs %)
              "Invalid choice."))
 
-(defn to-nil [s]
-  (if (= s "")
-    nil
-    s))

File src/red_tape/core.clj

   (:require [slingshot.slingshot :refer [try+ throw+]]))
 
 
-(defmacro map-for [& body]
+(defmacro map-for
+  "Like (for ...), but the body should return [k v] pairs that will be
+  turned into a map.
+
+  "
+  [& body]
   `(into {} (for ~@body)))
 
 
           [results form-errors] (clean-results results form-cleaners)]
       (if form-errors
         {:results nil
-         :errors {:form form-errors}
+         :errors {:red-tape/form form-errors}
          :valid false}
         {:results results
          :errors nil
   (let [blank (into {} (map #(vector % "") field-keys))]
     (merge blank initial)))
 
-(defn zip-map [a b]
+(defn zip-map
+  "Turn two vectors [k1 k2 ...] [v1 v2 ...] into a map {k1 v1 k2 v2 ...}."
+  [a b]
   (into {} (map vector a b)))
 
+(defn parse-fields
+  "Take a flat vector of fields and return a [fields form-cleaners] pair."
+  [fields]
+  (let [fields (into {} (map vec (partition 2 fields)))]
+    [(vec (dissoc fields :red-tape/form))
+     (get fields :red-tape/form)]))
+
+
 (defn form-guts
   "For internal use only.  You probably want form or defform.  Turn back now.
 
-  Return the guts of a form, suitable for splicing into (fn ..)
+  Return the guts of a form, suitable for splicing into (fn ...)
   or (defn name ...).
 
   "
-  [{:keys [arguments initial clean] :or {initial {} arguments []}} fields]
+  [{:keys [arguments initial] :or {initial {} arguments []}} fields]
   (let [arg-keys (map keyword arguments)
 
         ; Create the binding map, which is a map of keywords to symbols:
         ;
         ; [:f1 [a] :f2 [b c]]
         ;
-        ; into vector pairs like:
+        ; into a vector of field pairs, plus the form-level cleaners:
         ;
         ; [[:f1 [a]]
         ;  [:f2 [b c]]]
-        fields (mapv vec (partition 2 fields))
+        [fields form-cleaners] (parse-fields fields)
 
         ; Get a vector of just the field keys like [:f1 :f2].
         field-keys (mapv first fields)
 
-        ; A fresh form simply returns a map.
+        ; A fresh form simply returns a basic result map.
         fresh `{:fresh true
                 :arguments ~binding-map
                 :data (initial-data ~field-keys ~initial)
        (-> ~fields
          (zip-fields data#)
          process-fields
-         (process-result ~clean)
+         (process-result ~form-cleaners)
          (assoc :fresh false
                 :data data#
                 :arguments ~binding-map)))]))
 
 
 (defmacro form
-  [{:keys [arguments initial clean] :as options} & fields]
+  "Create an anonymous form.
+
+  form is to defform as fn is to defn.
+
+  "
+  [{:keys [arguments initial] :as options} & fields]
   `(fn ~@(form-guts options fields)))
 
 (defmacro defform
-  [form-name {:keys [arguments initial clean] :as options} & fields]
+  "Define a form.
+
+  The first argument is the name to def the form to.
+
+  The second argument is the option map.
+
+  The rest of the arguments are field and cleaner pairs.
+
+  See the full documentation for more information.
+
+  "
+  [form-name {:keys [arguments initial] :as options} & fields]
   `(defn ~form-name ~@(form-guts options fields)))
 

File test/red_tape/core_test.clj

 (ns red-tape.core-test
   (:require [clojure.test :refer :all]
             [red-tape.core :refer [defform]]
-            [red-tape.cleaners :as cs]))
+            [red-tape.cleaners :as cs]
+            [slingshot.slingshot :refer [throw+]]))
+
+
+(defn fresh-form
+  ([data]
+   (fresh-form data {}))
+  ([data arguments]
+   {:fresh true
+    :valid nil
+    :data data
+    :results nil
+    :arguments arguments
+    :errors nil}))
 
 
 (defform number-form {}
 (defform state-form {:arguments [states]}
   :state [clojure.string/trim (partial cs/choices states)])
 
+(defform initial-form {:initial {:x "42"}}
+  :x []
+  :y [])
+
+(defform dynamic-initial-form
+  {:arguments [x]
+   :initial {:a (first x)
+             :b (second x)}}
+  :a []
+  :b [])
+
+
+(defn a-b-match [data]
+  (if (= (:a data) (:b data))
+    data
+    (throw+ "No match!")))
+
+(defform form-cleaner-a-b {}
+  :a []
+  :b []
+  :red-tape/form a-b-match)
+
 
 (deftest test-number-form
+  (is (= (number-form)
+         (fresh-form {:n ""})))
+
+  (is (= (:results (number-form {"n" "10"})))
+      10)
+
   (are [n result]
        (= (:results (number-form {:n n}))
           {:n result})
        "-42" -42))
 
 (deftest test-numbers-form
+  (is (= (numbers-form)
+         (fresh-form {:n ""
+                      :m ""})))
+
   (are [n m rn rm]
        (= (:results (numbers-form {:m m :n n}))
           {:n rn :m rm})
        "     10"   10
        "1   "       1
        "   -42  " -42))
+
 (deftest test-state-form
   (are [available-states data result]
        (= (:results (state-form available-states {:state data}))
        #{"pa" "ny"} " ny"   "ny"
        #{"ny"}      " ny  " "ny"
        #{"ny"}      "ny"    "ny")
+
   (are [available-states data errors]
        (= (:errors (state-form available-states {:state data}))
           errors)
        #{"pa" "ny"} "nj" {:state "Invalid choice."}))
+(deftest test-initial-form
+  ; Initial data should be passed through to the initial :data map.
+  (is (= (initial-form)
+         (fresh-form {:x "42"
+                      :y ""})))
 
+  ; Initial data should be ignored when we have real data.
+  (is (= (initial-form {:x "1" :y "2"})
+         {:valid true
+          :fresh false
+          :data {:x "1" :y "2"}
+          :results {:x "1" :y "2"}
+          :arguments {}
+          :errors nil})))
+(deftest test-dynamic-initial-form
+  (is (= (dynamic-initial-form ["1" "2"])
+         (fresh-form {:a "1"
+                      :b "2"}
+                     {:x ["1" "2"]})))
+
+  (is (= (dynamic-initial-form ["1" "2"]
+                               {:a "3" :b "4"})
+         {:valid true
+          :fresh false
+          :data {:a "3" :b "4"}
+          :results {:a "3" :b "4"}
+          :arguments {:x ["1" "2"]}
+          :errors nil})))
+(deftest test-form-cleaner-a-b
+  (is (= (:results (form-cleaner-a-b {:a "foo" :b "foo"}))
+         {:a "foo" :b "foo"}))
+  (is (= (:errors (form-cleaner-a-b {:a "foo" :b "bar"}))
+         {:red-tape/form ["No match!"]}))
+  )