red-tape/cleaners/index.html @ fd88490d871a
cl-blt: Update site.
| author | Steve Losh <steve@stevelosh.com> |
|---|---|
| date | Fri, 24 Nov 2017 14:29:25 -0500 |
| 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">; =></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">; =></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">< </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">> </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">< </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">> </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">; =></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">; =></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">; =></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">; =></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>