red-tape/basics/index.html @ 0f6bab39c0f4
default tip
adopt: Update site.
author |
Steve Losh <steve@stevelosh.com> |
date |
Thu, 13 Jun 2024 13:05:28 -0400 |
parents |
69831b3947d7 |
children |
(none) |
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Basics / Red Tape</title>
<link rel="stylesheet" href="../_dmedia/tango.css"/>
<link rel="stylesheet/less" type="text/css" href="../_dmedia/style.less"/>
<script src="../_dmedia/less.js" type="text/javascript">
</script>
</head>
<body class="content">
<div class="wrap">
<header><h1><a href="..">Red Tape</a></h1></header>
<div class="markdown">
<h1 id="basics"><a href="">Basics</a></h1><p>Red Tape is a fairly simple library. It's designed to take raw form data
(strings), validate it, and turn it into useful data structures.</p>
<p>Red Tape does <em>not</em> handle rendering form fields into HTML. That's the job of
your templating library, and you always need to customize <code><input></code> tags anyway.</p>
<p>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
action.</p>
<div class="toc">
<ul>
<li><a href="#scaffolding">Scaffolding</a></li>
<li><a href="#defining-the-form">Defining the Form</a></li>
<li><a href="#using-the-form">Using the Form</a></li>
<li><a href="#cleaners">Cleaners</a></li>
<li><a href="#validation">Validation</a></li>
<li><a href="#built-in-cleaners">Built-In Cleaners</a></li>
<li><a href="#putting-it-all-together">Putting it All Together</a></li>
<li><a href="#summary">Summary</a></li>
</ul></div>
<h2 id="scaffolding">Scaffolding</h2>
<p>For this tutorial we'll create a Compojure app that allows people to submit
comments. Let's sketch out the normal structure of that now:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">ns </span><span class="nv">feedback</span>
<span class="p">(</span><span class="nf">require</span> <span class="p">[</span><span class="nv">compojure.core</span> <span class="ss">:refer</span> <span class="ss">:all</span><span class="p">]</span>
<span class="p">[</span><span class="nv">compojure.route</span> <span class="ss">:as</span> <span class="nv">route</span><span class="p">]</span>
<span class="p">[</span><span class="nv">hiccup.page</span> <span class="ss">:refer</span> <span class="p">[</span><span class="nv">html5</span><span class="p">]]</span>
<span class="p">[</span><span class="nv">ring.adapter.jetty</span> <span class="ss">:refer</span> <span class="p">[</span><span class="nv">run-jetty</span><span class="p">]]</span>
<span class="p">[</span><span class="nv">ring.middleware.params</span> <span class="ss">:refer</span> <span class="p">[</span><span class="nv">wrap-params</span><span class="p">]]))</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">page</span> <span class="p">[]</span>
<span class="p">(</span><span class="nf">html5</span>
<span class="p">[</span><span class="ss">:body</span>
<span class="p">[</span><span class="ss">:form</span> <span class="p">{</span><span class="ss">:method</span> <span class="s">"POST"</span> <span class="ss">:action</span> <span class="s">"/"</span><span class="p">}</span>
<span class="p">[</span><span class="ss">:label</span> <span class="s">"Who are you?"</span><span class="p">]</span>
<span class="p">[</span><span class="ss">:input</span> <span class="p">{</span><span class="ss">:type</span> <span class="s">"text"</span> <span class="ss">:name</span> <span class="s">"name"</span><span class="p">}]</span>
<span class="p">[</span><span class="ss">:label</span> <span class="s">"What do you want to say?"</span><span class="p">]</span>
<span class="p">[</span><span class="ss">:textarea</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"comment"</span><span class="p">}]]]))</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">save-feedback</span> <span class="p">[</span><span class="nv">from</span> <span class="nv">comment</span><span class="p">]</span>
<span class="c1">; In a real app this would save the string to a database, email</span>
<span class="c1">; it to someone, etc.</span>
<span class="p">(</span><span class="nb">println </span><span class="nv">from</span> <span class="s">"said:"</span> <span class="nv">comment</span><span class="p">))</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">handle-get</span> <span class="p">[]</span>
<span class="c1">; ...</span>
<span class="p">)</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">handle-post</span> <span class="p">[]</span>
<span class="c1">; ...</span>
<span class="p">)</span>
<span class="p">(</span><span class="nf">defroutes</span> <span class="nv">app</span>
<span class="p">(</span><span class="nf">GET</span> <span class="s">"/"</span> <span class="nv">request</span> <span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span><span class="p">))</span>
<span class="p">(</span><span class="nf">POST</span> <span class="s">"/"</span> <span class="nv">request</span> <span class="p">(</span><span class="nf">handle-post</span> <span class="nv">request</span><span class="p">)))</span>
<span class="p">(</span><span class="k">def </span><span class="nv">handler</span> <span class="p">(</span><span class="nb">-> </span><span class="nv">app</span>
<span class="nv">wrap-params</span><span class="p">))</span>
<span class="p">(</span><span class="kd">defonce </span><span class="nv">server</span>
<span class="p">(</span><span class="nf">run-jetty</span> <span class="o">#</span><span class="ss">'handler</span> <span class="p">{</span><span class="ss">:port</span> <span class="mi">3000</span><span class="p">}))</span>
</pre></div>
<p>That's about it for the boilerplate. The next step is to fill in the bodies of
<code>handle-get</code> and <code>handle-post</code>. We'll need to do a few things:</p>
<ul>
<li>Render the initial comment form when the user first GETs the page.</li>
<li>Validate incoming POST data to make sure it's sane.</li>
<li>If the data isn't valid, inform the user and re-render the form nicely so they
can fix it.</li>
<li>Once we've got valid data, clean it up and send it off to be saved.</li>
</ul>
<p>This is where Red Tape comes in.</p>
<h2 id="defining-the-form">Defining the Form</h2>
<p>The main part of Red Tape is the <code>defform</code> macro. Let's define a simple
feedback form:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">ns </span><span class="nv">feedback</span>
<span class="c1">; ...</span>
<span class="p">(</span><span class="nf">require</span> <span class="p">[</span><span class="nv">red-tape.core</span> <span class="ss">:refer</span> <span class="p">[</span><span class="nv">defform</span><span class="p">]]))</span>
<span class="p">(</span><span class="nf">defform</span> <span class="nv">feedback-form</span> <span class="p">{}</span>
<span class="ss">:name</span> <span class="p">[]</span>
<span class="ss">:comment</span> <span class="p">[])</span>
</pre></div>
<p><code>defform</code> takes a name, a map of form options, and a sequence of keywords and
vectors representing fields. We'll look at each of those parts in more detail
later, but for now let's actually <em>use</em> the form we've defined.</p>
<h2 id="using-the-form">Using the Form</h2>
<p>Defining a form results in a simple function that can be called with or without
data. Let's sketch out how our handler functions will look:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-get</span>
<span class="p">([</span><span class="nv">request</span><span class="p">]</span>
<span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span> <span class="p">(</span><span class="nf">feedback-form</span><span class="p">)))</span>
<span class="p">([</span><span class="nv">request</span> <span class="nv">form</span><span class="p">]</span>
<span class="p">(</span><span class="nf">page</span><span class="p">)))</span>
</pre></div>
<p>There are a couple of things going on here.</p>
<p>We've split the definition of <code>handle-get</code> into two pieces. The first piece
takes a request, builds the default feedback form and forwards those along to
the second piece, which actually renders the page. You'll see why we split it
up like that shortly.</p>
<p>Calling <code>(feedback-form)</code> without data returns a "result map" representing a fresh
form. It will look like this:</p>
<div class="codehilite"><pre><span class="p">{</span><span class="ss">:fresh</span> <span class="nv">true</span>
<span class="ss">:valid</span> <span class="nv">false</span>
<span class="ss">:arguments</span> <span class="p">{}</span>
<span class="ss">:data</span> <span class="p">{}</span>
<span class="ss">:results</span> <span class="nv">nil</span>
<span class="ss">:errors</span> <span class="nv">nil</span><span class="p">}</span>
</pre></div>
<p>We'll see how to use this later. Let's move on to <code>handle-post</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-post</span> <span class="p">[</span><span class="nv">request</span><span class="p">]</span>
<span class="p">(</span><span class="k">let </span><span class="p">[</span><span class="nv">data</span> <span class="p">(</span><span class="ss">:params</span> <span class="nv">request</span><span class="p">)</span>
<span class="nv">form</span> <span class="p">(</span><span class="nf">feedback-form</span> <span class="nv">data</span><span class="p">)]</span>
<span class="c1">; ...))</span>
</pre></div>
<p><code>handle-post</code> takes the raw HTTP POST data (from <code>(:params request)</code>) and passes
it through the feedback form. Once again this results in a map. Assuming the
user entered the name "Steve" and the comment "Hello!", the resulting map will
look like this:</p>
<div class="codehilite"><pre><span class="p">{</span><span class="ss">:fresh</span> <span class="nv">false</span>
<span class="ss">:valid</span> <span class="nv">true</span>
<span class="ss">:arguments</span> <span class="p">{}</span>
<span class="ss">:data</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"Steve"</span>
<span class="ss">:comment</span> <span class="s">"Hello!"</span><span class="p">}</span>
<span class="ss">:results</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"Steve"</span>
<span class="ss">:comment</span> <span class="s">"Hello!"</span><span class="p">}</span>
<span class="ss">:errors</span> <span class="nv">nil</span><span class="p">}</span>
</pre></div>
<p>In a nutshell, this is all Red Tape does. You define form functions using
<code>defform</code>, and those functions take in data and turn it into a result map like
this.</p>
<p>Let's add a bit of data cleaning to the form to get something more useful.</p>
<h2 id="cleaners">Cleaners</h2>
<p>Every field you define in a <code>defform</code> also gets a vector of "cleaners"
associated with it. A cleaner is a vanilla Clojure function that takes one
argument (the incoming value) and returns a new value (the outgoing result).</p>
<p>Let's see this in action by modifying our form to strip leading and trailing
whitespace from the user's name automatically:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">feedback-form</span> <span class="p">{}</span>
<span class="ss">:name</span> <span class="p">[</span><span class="nv">clojure.string/trim</span><span class="p">]</span>
<span class="ss">:comment</span> <span class="p">[])</span>
</pre></div>
<p><code>clojure.string/trim</code> is just a normal Clojure function that trims off
whitespace. Let's imagine that the user entered <code>Steve</code> as their name
this time. Calling <code>(feedback-form data)</code> now results in the following map:</p>
<div class="codehilite"><pre><span class="p">{</span><span class="ss">:fresh</span> <span class="nv">false</span>
<span class="ss">:valid</span> <span class="nv">true</span>
<span class="ss">:arguments</span> <span class="p">{}</span>
<span class="ss">:data</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">" Steve "</span> <span class="ss">:comment</span> <span class="s">"Hello!"</span><span class="p">}</span>
<span class="ss">:results</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"Steve"</span> <span class="ss">:comment</span> <span class="s">"Hello!"</span><span class="p">}</span>
<span class="ss">:errors</span> <span class="nv">nil</span><span class="p">}</span>
</pre></div>
<p>The <code>:data</code> in the result map still contains the raw data the user entered, but
the <code>:results</code> have had their values passed through their cleaners first.</p>
<p>You can define as many cleaners as you want for each field. The data will be
threaded through them in order, much like the <code>-></code> macro. This lets you define
simple cleaning functions and combine them as needed. For example:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">feedback-form</span> <span class="p">{}</span>
<span class="ss">:name</span> <span class="p">[</span><span class="nv">clojure.string/trim</span>
<span class="nv">clojure.string/lower-case</span><span class="p">]</span>
<span class="ss">:comment</span> <span class="p">[</span><span class="nv">clojure.string/trim</span><span class="p">])</span>
<span class="p">(</span><span class="nf">feedback-form</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">" Steve "</span> <span class="ss">:comment</span> <span class="s">" Hello! "</span><span class="p">})</span>
<span class="c1">; =></span>
<span class="p">{</span><span class="ss">:fresh</span> <span class="nv">false</span>
<span class="ss">:valid</span> <span class="nv">true</span>
<span class="ss">:data</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">" Steve "</span> <span class="ss">:comment</span> <span class="s">" Hello! "</span><span class="p">}</span>
<span class="ss">:results</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"steve"</span> <span class="ss">:comment</span> <span class="s">"Hello!"</span><span class="p">}</span>
<span class="c1">; ...</span>
<span class="p">}</span>
</pre></div>
<p>Here we're trimming the name and then lowercasing it, and trimming the comment
as well (but not lowercasing that).</p>
<h2 id="validation">Validation</h2>
<p>Cleaners also serve another purpose. If a cleaner function throws an Exception,
the value won't progress any further, and the result map will be marked as
invalid.</p>
<p>Let's look at an example:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">age-form</span> <span class="p">{}</span>
<span class="ss">:age</span> <span class="p">[</span><span class="nv">clojure.string/trim</span>
<span class="o">#</span><span class="p">(</span><span class="nf">Long.</span> <span class="nv">%</span><span class="p">)])</span>
</pre></div>
<p>If we call this form with a number, everything is fine:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">age-form</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"27"</span><span class="p">})</span>
<span class="c1">; =></span>
<span class="p">{</span><span class="ss">:fresh</span> <span class="nv">false</span>
<span class="ss">:valid</span> <span class="nv">true</span>
<span class="ss">:data</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"27"</span><span class="p">}</span>
<span class="ss">:results</span> <span class="p">{</span><span class="ss">:age</span> <span class="mi">27</span><span class="p">}</span>
<span class="ss">:errors</span> <span class="nv">nil</span><span class="p">}</span>
</pre></div>
<p>But if we try to feed it garbage:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">age-form</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"cats"</span><span class="p">})</span>
<span class="c1">; =></span>
<span class="p">{</span><span class="ss">:fresh</span> <span class="nv">false</span>
<span class="ss">:valid</span> <span class="nv">false</span>
<span class="ss">:data</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"cats"</span><span class="p">}</span>
<span class="ss">:results</span> <span class="nv">nil</span>
<span class="ss">:errors</span> <span class="p">{</span><span class="ss">:age</span> <span class="nv"><NumberFormatException</span><span class="err">:</span> <span class="nv">...></span><span class="p">}}</span>
</pre></div>
<p>There are a few things to see here. If any cleaner function throws an
Exception, the resulting map will have <code>:valid</code> set to <code>false</code>.</p>
<p>There will also be no <code>:results</code> entry in an invalid result. You only get
<code>:results</code> if your <em>entire</em> form is valid. This is to help prevent you from
accidentally using the results of a form with invalid data.</p>
<p>The <code>:errors</code> map will map field names to the exception their cleaners threw.
This happens on a per-field basis, so you can have separate errors for each
field.</p>
<p>Red Tape uses Slingshot's <code>try+</code> to catch exceptions, so if you want to you can
use <code>throw+</code> to throw errors in an easier-to-manage way and they'll be caught
just fine.</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">ensure-not-immortal</span> <span class="p">[</span><span class="nv">age</span><span class="p">]</span>
<span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">> </span><span class="nv">age</span> <span class="mi">150</span><span class="p">)</span>
<span class="p">(</span><span class="nf">throw+</span> <span class="s">"I think you're lying!"</span><span class="p">)</span>
<span class="nv">age</span><span class="p">))</span>
<span class="p">(</span><span class="nf">defform</span> <span class="nv">age-form</span> <span class="p">{}</span>
<span class="ss">:age</span> <span class="p">[</span><span class="nv">clojure.string/trim</span>
<span class="o">#</span><span class="p">(</span><span class="nf">Long.</span> <span class="nv">%</span><span class="p">)</span>
<span class="nv">ensure-not-immortal</span><span class="p">])</span>
<span class="p">(</span><span class="nf">age-form</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"1000"</span><span class="p">})</span>
<span class="c1">; =></span>
<span class="p">{</span><span class="ss">:fresh</span> <span class="nv">false</span>
<span class="ss">:valid</span> <span class="nv">false</span>
<span class="ss">:data</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"1000"</span><span class="p">}</span>
<span class="ss">:results</span> <span class="nv">nil</span>
<span class="ss">:errors</span> <span class="p">{</span><span class="ss">:age</span> <span class="s">"I think you're lying!"</span><span class="p">}}</span>
</pre></div>
<p>Notice how <code>ensure-not-immortal</code> expected a number and not a String. This is
fine because we still kept the <code>#(Long. %)</code> cleaner to handle that conversion.</p>
<p>Finally, also notice that the <code>:data</code> entry in the result map is present and
contains the data the user entered, even though it turned out to be invalid.
We'll use this later when we want to rerender the form, so the user doesn't have
to type all the data again.</p>
<h2 id="built-in-cleaners">Built-In Cleaners</h2>
<p>Red Tape contains a number of useful cleaner functions pre-defined in the
<code>red-tape.cleaners</code> namespace.</p>
<p>We'll use <code>red-tape.cleaners/non-blank</code> in this tutorial. <code>non-blank</code> is
a simple cleaner that throws an exception if it receives an empty string, or
otherwise passes through the data unchanged.</p>
<p>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):</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">ns </span><span class="nv">feedback</span>
<span class="c1">; ...</span>
<span class="p">(</span><span class="nf">require</span> <span class="p">[</span><span class="nv">red-tape.cleaners</span> <span class="ss">:as</span> <span class="nv">cleaners</span><span class="p">]))</span>
<span class="p">(</span><span class="nf">defform</span> <span class="nv">feedback-form</span> <span class="p">{}</span>
<span class="ss">:name</span> <span class="p">[</span><span class="nv">clojure.string/trim</span><span class="p">]</span>
<span class="ss">:comment</span> <span class="p">[</span><span class="nv">clojure.string/trim</span> <span class="nv">cleaners/non-blank</span><span class="p">])</span>
</pre></div>
<p>Notice that we trim whitespace <em>before</em> checking for a non-blank string, so
a comment of all whitespace would result in an error.</p>
<h2 id="putting-it-all-together">Putting it All Together</h2>
<p>Now that we've seen how to clean and validate, we can finally connect the
missing pieces to our feedback form.</p>
<p>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:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">page</span> <span class="p">[</span><span class="nv">form</span><span class="p">]</span>
<span class="p">(</span><span class="nf">html5</span>
<span class="p">[</span><span class="ss">:body</span>
<span class="p">[</span><span class="ss">:form</span> <span class="p">{</span><span class="ss">:method</span> <span class="s">"POST"</span> <span class="ss">:action</span> <span class="s">"/"</span><span class="p">}</span>
<span class="p">[</span><span class="ss">:label</span> <span class="s">"Who are you?"</span><span class="p">]</span>
<span class="p">[</span><span class="ss">:input</span> <span class="p">{</span><span class="ss">:type</span> <span class="s">"text"</span> <span class="ss">:name</span> <span class="s">"name"</span>
<span class="ss">:value</span> <span class="p">(</span><span class="nf">get-in</span> <span class="nv">form</span> <span class="p">[</span><span class="ss">:data</span> <span class="ss">:name</span><span class="p">])}]</span>
<span class="p">[</span><span class="ss">:label</span> <span class="s">"What do you want to say?"</span><span class="p">]</span>
<span class="p">[</span><span class="ss">:textarea</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"comment"</span><span class="p">}</span> <span class="p">(</span><span class="nf">get-in</span> <span class="nv">form</span> <span class="p">[</span><span class="ss">:data</span> <span class="ss">:comment</span><span class="p">])]]]))</span>
</pre></div>
<p>Notice how we pull the values of each field out of the <code>:data</code> entry in the form
result map.</p>
<p>Now we can write the final GET handler:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-get</span>
<span class="p">([</span><span class="nv">request</span><span class="p">]</span>
<span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span> <span class="p">(</span><span class="nf">feedback-form</span><span class="p">)))</span>
<span class="p">([</span><span class="nv">request</span> <span class="nv">form</span><span class="p">]</span>
<span class="p">(</span><span class="nf">page</span> <span class="nv">form</span><span class="p">)))</span>
</pre></div>
<p>And the POST handler:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-post</span> <span class="p">[</span><span class="nv">request</span><span class="p">]</span>
<span class="p">(</span><span class="k">let </span><span class="p">[</span><span class="nv">data</span> <span class="p">(</span><span class="ss">:params</span> <span class="nv">request</span><span class="p">)</span>
<span class="nv">form</span> <span class="p">(</span><span class="nf">feedback-form</span> <span class="nv">data</span><span class="p">)]</span>
<span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="ss">:valid</span> <span class="nv">form</span><span class="p">)</span>
<span class="p">(</span><span class="k">let </span><span class="p">[{</span><span class="ss">:keys</span> <span class="p">[</span><span class="nb">name </span><span class="nv">comment</span><span class="p">]}</span> <span class="p">(</span><span class="ss">:results</span> <span class="nv">form</span><span class="p">)]</span>
<span class="p">(</span><span class="nf">save-feedback</span> <span class="nb">name </span><span class="nv">comment</span><span class="p">)</span>
<span class="p">(</span><span class="nf">redirect</span> <span class="s">"/"</span><span class="p">))</span>
<span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span> <span class="nv">form</span><span class="p">))))</span>
</pre></div>
<p>We use the form to process the raw data, and then examine the result. If it is
valid, we save the feedback by using the cleaned <code>:results</code> and we're done.</p>
<p>If it's <em>not</em> valid, we use the GET handler to re-render the form without
redirecting. We pass along our <em>invalid</em> form as we do that, so that when the
GET handler calls the <code>page</code> and uses the <code>:data</code> it will fill in the fields
correctly so the user doesn't have to retype everything.</p>
<h2 id="summary">Summary</h2>
<p>That was a lot to cover, but now you've seen the basic Red Tape workflow! Most
of the time you'll be doing what we just finished:</p>
<ul>
<li>Defining the form.</li>
<li>Defining a GET handler that creates a blank form.</li>
<li>Defining a GET handler that takes a form (either blank or invalid) and renders
it to HTML.</li>
<li>Defining a POST handler that runs data through the form, examines the result,
and does the appropriate thing depending on whether it's valid or not.</li>
</ul>
<p>Now that you've got the general idea, it's time to look at a few topics in more
detail. Start with the <a href="../input/">form input</a> guide.</p>
</div>
<footer><p>Made and <a href="http://sjl.bitbucket.org/d/">documented</a> with love by <a href="http://stevelosh.com">Steve
Losh</a>.</p>
<p><br/><a id="rochester-made" href="http://rochestermade.com" title="Rochester Made"><img src="http://rochestermade.com/media/images/rochester-made-dark-on-light.png" alt="Rochester Made" title="Rochester Made"/></a></p></footer>
</div>
</body>
</html>