red-tape/cleaners/index.html @ 15d3b832fdc5 default tip

cl-digraph: Update site.
author Steve Losh <steve@stevelosh.com>
date Wed, 21 Jun 2023 15:21:12 -0400
parents 69831b3947d7
children (none)
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Cleaners / 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="cleaners"><a href="">Cleaners</a></h1><p>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.</p>
<div class="toc">
<ul>
<li><a href="#cleaners-are-functions">Cleaners are Functions</a></li>
<li><a href="#validation-errors">Validation Errors</a></li>
<li><a href="#optional-fields">Optional Fields</a></li>
<li><a href="#form-level-cleaners">Form-Level Cleaners</a></li>
<li><a href="#built-in-cleaners">Built-In Cleaners</a></li>
<li><a href="#results">Results</a></li>
</ul></div>
<h2 id="cleaners-are-functions">Cleaners are Functions</h2>
<p>Cleaners are plain old Clojure functions -- there's nothing special about them.
They take one argument (the data to clean) and return a result.</p>
<p>Let's look at a few examples.  First we have a cleaner function that takes
a value and turns it into a Long:</p>
<div class="codehilite"><pre><span class="c1">; A cleaner to turn the raw string into a Long.</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">to-long</span> <span class="p">[</span><span class="nv">v</span><span class="p">]</span>
  <span class="p">(</span><span class="nf">Long.</span> <span class="nv">v</span><span class="p">))</span>
</pre></div>


<p>The next cleaner function takes a user ID, looks up the user in a "database",
and returns the user.  We'll talk about the <code>throw+</code> in the next section.</p>
<div class="codehilite"><pre><span class="c1">; A cleaner to take a Long user ID and look up the</span>
<span class="c1">; user in a database.</span>
<span class="p">(</span><span class="k">def </span><span class="nv">users</span> <span class="p">{</span><span class="mi">1</span> <span class="s">"Steve"</span><span class="p">})</span>

<span class="p">(</span><span class="kd">defn </span><span class="nv">to-user</span> <span class="p">[</span><span class="nv">id</span><span class="p">]</span>
  <span class="p">(</span><span class="k">let </span><span class="p">[</span><span class="nv">user</span> <span class="p">(</span><span class="nb">get </span><span class="nv">users</span> <span class="nv">id</span><span class="p">)]</span>
    <span class="p">(</span><span class="k">if </span><span class="nv">user</span>
      <span class="nv">user</span>
      <span class="p">(</span><span class="nf">throw+</span> <span class="s">"Invalid user ID!"</span><span class="p">))))</span>
</pre></div>


<p>Now we can use these two cleaners in a simple form:</p>
<div class="codehilite"><pre><span class="c1">; Use both of these cleaners to first turn the input</span>
<span class="c1">; string into a long, then into a user.</span>
<span class="p">(</span><span class="nf">defform</span> <span class="nv">user-form</span> <span class="p">{}</span>
  <span class="ss">:user</span> <span class="p">[</span><span class="nv">to-long</span> <span class="nv">to-user</span><span class="p">])</span>

<span class="c1">; Now we'll call the form with some data.</span>
<span class="p">(</span><span class="nf">user-form</span> <span class="p">{</span><span class="s">"user"</span> <span class="s">"1"</span><span class="p">})</span>
<span class="c1">; =&gt;</span>
<span class="p">{</span><span class="ss">:valid</span> <span class="nv">true</span>
 <span class="ss">:results</span> <span class="p">{</span><span class="ss">:user</span> <span class="s">"Steve"</span><span class="p">}</span>
 <span class="nv">...</span><span class="p">}</span>
</pre></div>


<h2 id="validation-errors">Validation Errors</h2>
<p>Cleaners can report a validation error by throwing an exception, or by using
Slingshot's <code>throw+</code> to throw <em>anything</em>.</p>
<p>If a cleaner throws something, the result map's <code>:valid</code> entry will be <code>false</code>
and the <code>:errors</code> entry will contain whatever was thrown.  Continuing the
example above:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">user-form</span> <span class="p">{</span><span class="s">"user"</span> <span class="s">"400"</span><span class="p">})</span>
<span class="c1">; =&gt;</span>
<span class="p">{</span><span class="ss">:valid</span> <span class="nv">false</span>
 <span class="ss">:errors</span> <span class="p">{</span><span class="ss">:user</span> <span class="s">"Invalid user ID!"</span><span class="p">}</span>
 <span class="nv">...</span><span class="p">}</span>
</pre></div>


<p>What happened here?</p>
<p>First, the string <code>"400"</code> was given to the first cleaner, which turned it into
the long <code>400</code>.</p>
<p>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 <code>throw+</code> to throw a string as
an error, so the form was marked as invalid.</p>
<h2 id="optional-fields">Optional Fields</h2>
<p>Sometimes you want to be have a field that is optional, but when it's given you
want to transform it.</p>
<p>You <em>could</em> do this by adapting your cleaner functions like this:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">user-profile</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[</span><span class="nv">...</span><span class="p">]</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="nv">...</span><span class="p">]</span>
  <span class="ss">:bio</span> <span class="p">[</span><span class="o">#</span><span class="p">(</span><span class="nb">when-let </span><span class="p">[</span><span class="nv">bio</span> <span class="nv">%</span><span class="p">]</span> <span class="nv">bio</span><span class="p">)</span>
        <span class="o">#</span><span class="p">(</span><span class="nb">when-let </span><span class="p">[</span><span class="nv">bio</span> <span class="nv">%</span><span class="p">]</span>
           <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">&lt; </span><span class="p">(</span><span class="nf">length</span> <span class="nv">bio</span><span class="p">)</span> <span class="mi">10</span><span class="p">)</span>
             <span class="p">(</span><span class="nf">throw+</span> <span class="s">"If given, must be at least 10 characters."</span><span class="p">)</span>
             <span class="nv">bio</span><span class="p">))</span>
        <span class="o">#</span><span class="p">(</span><span class="nb">when-let </span><span class="p">[</span><span class="nv">bio</span> <span class="nv">%</span><span class="p">]</span>
           <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">&gt; </span><span class="p">(</span><span class="nf">length</span> <span class="nv">bio</span><span class="p">)</span> <span class="mi">2000</span><span class="p">)</span>
             <span class="p">(</span><span class="nf">throw+</span> <span class="s">"Must be under 2000 characters."</span><span class="p">)</span>
             <span class="nv">bio</span><span class="p">))])</span>
</pre></div>


<p>This will certainly work, but it means you have to convert the empty string to
<code>nil</code> manually and then do a lot of <code>when-let</code>ing in your cleaners to pass the
<code>nil</code> through to the end.</p>
<p>You can avoid this by marking the cleaners as <code>:red-tape/optional</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">user-profile</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[</span><span class="nv">...</span><span class="p">]</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="nv">...</span><span class="p">]</span>
  <span class="ss">:bio</span> <span class="o">^</span><span class="ss">:red-tape/optional</span> <span class="p">[</span>
    <span class="o">#</span><span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">&lt; </span><span class="p">(</span><span class="nf">length</span> <span class="nv">%</span><span class="p">)</span> <span class="mi">10</span><span class="p">)</span>
       <span class="p">(</span><span class="nf">throw+</span> <span class="s">"If given, must be at least 10 characters."</span><span class="p">)</span>
       <span class="nv">%</span><span class="p">)</span>
    <span class="o">#</span><span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">&gt; </span><span class="p">(</span><span class="nf">length</span> <span class="nv">%</span><span class="p">)</span> <span class="mi">2000</span><span class="p">)</span>
       <span class="p">(</span><span class="nf">throw+</span> <span class="s">"Must be under 2000 characters."</span><span class="p">)</span>
       <span class="nv">%</span><span class="p">)])</span>
</pre></div>


<h2 id="form-level-cleaners">Form-Level Cleaners</h2>
<p>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.</p>
<p>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, <em>after</em> the field-level cleaners have been run.</p>
<p>Note that if any individual fields had errors, the form-level cleaners will
<em>not</em> be run.  It doesn't make sense to run them on garbage input.</p>
<p>They can throw errors just like field-level cleaners too.</p>
<p>Let's look at how to use form-level cleaners with a simple example:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">new-passwords-match</span> <span class="p">[</span><span class="nv">form-data</span><span class="p">]</span>
  <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">not= </span><span class="p">(</span><span class="ss">:new-password-1</span> <span class="nv">form-data</span><span class="p">)</span>
            <span class="p">(</span><span class="ss">:new-password-2</span> <span class="nv">form-data</span><span class="p">))</span>
    <span class="p">(</span><span class="nf">throw+</span> <span class="s">"New passwords do not match!"</span><span class="p">)</span>
    <span class="nv">form-data</span><span class="p">))</span>

<span class="p">(</span><span class="nf">defform</span> <span class="nv">change-password-form</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[]</span>
  <span class="ss">:old-password</span> <span class="p">[]</span>
  <span class="ss">:new-password-1</span> <span class="p">[]</span>
  <span class="ss">:new-password-2</span> <span class="p">[]</span>
  <span class="ss">:red-tape/form</span> <span class="nv">new-passwords-match</span><span class="p">)</span>

<span class="p">(</span><span class="nf">change-password-form</span> <span class="p">{</span><span class="ss">:user-id</span> <span class="s">"101"</span>
                       <span class="ss">:old-password</span> <span class="s">"foo"</span>
                       <span class="ss">:new-password-1</span> <span class="s">"a"</span>
                       <span class="ss">:new-password-2</span> <span class="s">"b"</span><span class="p">})</span>
<span class="c1">; =&gt;</span>
<span class="p">{</span><span class="ss">:valid</span> <span class="nv">false</span>
 <span class="ss">:errors</span> <span class="p">{</span><span class="ss">:red-tape/form</span> <span class="o">#</span><span class="p">{</span><span class="s">"New passwords do not match!"</span><span class="p">}}}</span>
</pre></div>


<p>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).</p>
<p>If the new password fields match, the function returns the map of data.  In this
case it doesn't modify it at all, but it could if we wanted to.</p>
<p>If the new passwords don't match, an error is thrown with Slingshot's <code>throw+</code>.</p>
<p>Next we define the form.  The form-level cleaners are specified by attaching
them to the special <code>:red-tape/form</code> "field".</p>
<p>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.</p>
<p>The first way is to give a single function like we did in the example:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">foo</span> <span class="p">{}</span>
  <span class="nv">...</span>
  <span class="ss">:red-tape/form</span> <span class="nv">my-cleaner</span><span class="p">)</span>
</pre></div>


<p>If you only have one form-level cleaner this is the simplest way to go.</p>
<p>The second option is to give a vector of functions, just like field cleaners:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">foo</span> <span class="p">{}</span>
  <span class="nv">...</span>
  <span class="ss">:red-tape/form</span> <span class="p">[</span><span class="nv">my-cleaner-1</span> <span class="nv">my-cleaner-2</span><span class="p">])</span>
</pre></div>


<p>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.</p>
<p>Finally, you can give a set containing zero or more entries of either of the
first two types: </p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">foo</span> <span class="p">{}</span>
  <span class="nv">...</span>
  <span class="ss">:red-tape/form</span> <span class="o">#</span><span class="p">{</span><span class="nv">my-standalone-cleaner</span>
                   <span class="p">[</span><span class="nv">my-cleaner-part-1</span> <span class="nv">my-cleaner-part-2</span><span class="p">]})</span>
</pre></div>


<p>Each entry in the set will be evaluated according to the rules above, and its
output fed into the other entries.</p>
<p>This happens in an unspecified order, so you should only use a set to define
form-level cleaners that explicitly do <em>not</em> 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.</p>
<p>The last thing to notice here is that the form-level errors are returned as
a <em>set</em> in the result map.  This is because Red Tape will return <em>all</em> the
errors for each entry in the set of cleaners at once.  For example:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">new-passwords-match</span> <span class="p">[</span><span class="nv">form-data</span><span class="p">]</span>
  <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nb">not= </span><span class="p">(</span><span class="ss">:new-password-1</span> <span class="nv">form-data</span><span class="p">)</span>
            <span class="p">(</span><span class="ss">:new-password-2</span> <span class="nv">form-data</span><span class="p">))</span>
    <span class="p">(</span><span class="nf">throw+</span> <span class="s">"New passwords do not match!"</span><span class="p">)</span>
    <span class="nv">form-data</span><span class="p">))</span>

<span class="p">(</span><span class="kd">defn </span><span class="nv">old-password-is-correct</span> <span class="p">[</span><span class="nv">form-data</span><span class="p">]</span>
  <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="nf">check-password</span> <span class="p">(</span><span class="ss">:user-id</span> <span class="nv">form</span><span class="p">)</span> <span class="p">(</span><span class="ss">:old-password</span> <span class="nv">form</span><span class="p">)))</span>
    <span class="nv">form-data</span>
    <span class="p">(</span><span class="nf">throw+</span> <span class="s">"Current password is not correct!"</span><span class="p">))</span>

<span class="p">(</span><span class="nf">defform</span> <span class="nv">change-password-form</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[]</span>
  <span class="ss">:old-password</span> <span class="p">[]</span>
  <span class="ss">:new-password-1</span> <span class="p">[]</span>
  <span class="ss">:new-password-2</span> <span class="p">[]</span>
  <span class="ss">:red-tape/form</span> <span class="o">#</span><span class="p">{</span><span class="nv">old-password-is-correct</span> <span class="nv">new-passwords-match</span><span class="p">})</span>

<span class="p">(</span><span class="nf">change-password-form</span> <span class="p">{</span><span class="ss">:user-id</span> <span class="s">"101"</span>
                       <span class="ss">:old-password</span> <span class="s">"wrong"</span>
                       <span class="ss">:new-password-1</span> <span class="s">"a"</span>
                       <span class="ss">:new-password-2</span> <span class="s">"b"</span><span class="p">})</span>
<span class="c1">; =&gt;</span>
<span class="p">{</span><span class="ss">:valid</span> <span class="nv">false</span>
 <span class="ss">:errors</span> <span class="p">{</span><span class="ss">:red-tape/form</span> <span class="p">[</span><span class="s">"Current password is not correct!"</span>
                          <span class="s">"New passwords do not match!"</span><span class="p">]}}</span>
</pre></div>


<p>Since the form-level cleaners were both specified in a set, Red Tape knows that
one doesn't depend on the other.  Even though one of them failed, Red Tape will
still run the others and return <em>all</em> the errors so you can show them all to the
user at once.  Otherwise the user would have to tediously fix one error at
a time and submit to see if there were any other problems.</p>
<p>One last thing: form-level cleaners can change the values in the map they return
as much as they like, but they should <strong>not</strong> add or remove entries from it.
It's <em>probably</em> okay to add entries as long as they won't conflict with anything
else (i.e.: use a namespaced keyword) but the author makes no guarantees about
that.</p>
<h2 id="built-in-cleaners">Built-In Cleaners</h2>
<p>Red Tape contains a number of common cleaners in <code>red-tape.cleaners</code>.  There are
also some handy macros for making your own cleaners.</p>
<p><code>ensure-is</code> is a macro that takes a value, a predicate, and an error message.
If the value satisfies the predicate, that value is passed straight through.
Otherwise the error is thrown:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">user-profile</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[</span><span class="nv">...</span>
            <span class="o">#</span><span class="p">(</span><span class="nf">ensure-is</span> <span class="nv">%</span> <span class="nb">pos? </span><span class="s">"Invalid ID."</span><span class="p">)</span>
            <span class="nv">...</span><span class="p">]</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="nv">...</span><span class="p">]</span>
  <span class="ss">:bio</span> <span class="o">^</span><span class="ss">:red-tape/optional</span> <span class="p">[</span><span class="nv">...</span><span class="p">])</span>
</pre></div>


<p><code>ensure-not</code> passes the value through if it does <em>not</em> satisfy the predicate,
and throws the error if it <em>does</em>.</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">user-profile</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[</span><span class="nv">...</span><span class="p">]</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="nv">...</span>
             <span class="o">#</span><span class="p">(</span><span class="nf">ensure-not</span> <span class="nv">%</span> <span class="o">#</span><span class="p">{</span><span class="s">"admin"</span> <span class="s">"administrator"</span><span class="p">}</span>
                          <span class="s">"That username is reserved, sorry."</span><span class="p">)</span>
             <span class="nv">...</span><span class="p">]</span>
  <span class="ss">:bio</span> <span class="o">^</span><span class="ss">:red-tape/optional</span> <span class="p">[</span><span class="nv">...</span><span class="p">])</span>
</pre></div>


<p><code>red-tape.cleaners</code> also contains some pre-made cleaners that you'll probably
find useful:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">ns </span><span class="nv">...</span>
  <span class="p">(</span><span class="ss">: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">user-profile</span> <span class="p">{}</span>
  <span class="ss">:user-id</span> <span class="p">[</span><span class="nv">cleaners/to-long</span>
            <span class="nv">cleaners/positive</span>
            <span class="nv">...</span><span class="p">]</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="nv">cleaners/non-blank</span>
             <span class="o">#</span><span class="p">(</span><span class="nf">cleaners/length</span> <span class="mi">3</span> <span class="mi">20</span> <span class="nv">%</span><span class="p">)</span>
             <span class="nv">...</span><span class="p">]</span>
  <span class="ss">:bio</span> <span class="o">^</span><span class="ss">:red-tape/optional</span> <span class="p">[</span><span class="o">#</span><span class="p">(</span><span class="nf">cleaners/max-length</span> <span class="mi">2000</span> <span class="nv">%</span><span class="p">)]</span>
  <span class="ss">:state</span> <span class="p">[</span><span class="nv">cleaners/non-blank</span>
          <span class="nv">clojure.string/upper-case</span>
          <span class="o">#</span><span class="p">(</span><span class="nf">cleaners/choices</span> <span class="o">#</span><span class="p">{</span><span class="s">"NY"</span> <span class="s">"PA"</span> <span class="s">"OR"</span> <span class="nv">...</span><span class="p">})])</span>
</pre></div>


<p>Most of the built-in cleaners take an extra argument that lets you provide
a custom error message when they fail:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defform</span> <span class="nv">signup-form</span> <span class="p">{}</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="o">#</span><span class="p">(</span><span class="nf">cleaners/matches</span> <span class="o">#</span><span class="s">"[a-zA-Z0-9]+"</span> <span class="nv">%</span><span class="p">)])</span>

<span class="p">(</span><span class="nf">signup-form</span> <span class="p">{</span><span class="ss">:username</span> <span class="s">"cats and dogs!"</span><span class="p">})</span>
<span class="c1">; =&gt;</span>
<span class="p">{</span><span class="nv">...</span>
 <span class="ss">:errors</span> <span class="p">{</span><span class="ss">:username</span> <span class="s">"Invalid format."</span><span class="p">}</span>
 <span class="nv">...</span><span class="p">}</span>

<span class="p">(</span><span class="nf">defform</span> <span class="nv">better-signup-form</span> <span class="p">{}</span>
  <span class="ss">:username</span> <span class="p">[</span><span class="o">#</span><span class="p">(</span><span class="nf">cleaners/matches</span> <span class="o">#</span><span class="s">"[a-zA-Z0-9]+"</span> <span class="nv">%</span>
               <span class="s">"Username may contain only letters and numbers."</span><span class="p">)])</span>

<span class="p">(</span><span class="nf">signup-form</span> <span class="p">{</span><span class="ss">:username</span> <span class="s">"cats and dogs!"</span><span class="p">})</span>
<span class="c1">; =&gt;</span>
<span class="p">{</span><span class="nv">...</span>
 <span class="ss">:errors</span> <span class="p">{</span><span class="ss">:username</span> <span class="s">"Username may contain only letters and numbers."</span><span class="p">}</span>
 <span class="nv">...</span><span class="p">}</span>
</pre></div>


<p>See the <a href="../reference">Reference</a> section for the full list of built-in
cleaners.</p>
<h2 id="results">Results</h2>
<p>Once all cleaners have been run on the data, the results (or errors) will be
returned as a result map.  Read the <a href="../result-maps/">result maps</a> guide for
more information.</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>