# sjl.bitbucket.org / red-tape / cleaners / index.html

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308``` ```Cleaners / Red Tape

Red Tape

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.

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:

; 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.

; 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:

; 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:

(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.

Optional Fields

Sometimes you want to be have a field that is optional, but when it's given you want to transform it.

(defform user-profile {}   :user-id [...]   :username [...]   :bio [#(when-let [bio %] bio)         #(when-let [bio %]            (if (< (length bio) 10)              (throw+ "If given, must be at least 10 characters.")              bio))         #(when-let [bio %]            (if (> (length bio) 2000)              (throw+ "Must be under 2000 characters.")              bio))])

This will certainly work, but it means you have to convert the empty string to nil manually and then do a lot of when-leting in your cleaners to pass the nil through to the end.

You can avoid this by marking the cleaners as :red-tape/optional:

(defform user-profile {}   :user-id [...]   :username [...]   :bio ^:red-tape/optional [     #(if (< (length %) 10)        (throw+ "If given, must be at least 10 characters.")        %)     #(if (> (length %) 2000)        (throw+ "Must be under 2000 characters.")        %)])

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:

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 it 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:

(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:

(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:

(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.

The last thing to notice here is that the form-level errors are returned as a set in the result map. This is because Red Tape will return all the errors for each entry in the set of cleaners at once. For example:

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 all 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.

One last thing: form-level cleaners can change the values in the map they return as much as they like, but they should not add or remove entries from it. It's probably 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.

Built-In Cleaners

Red Tape contains a number of common cleaners in red-tape.cleaners. There are also some handy macros for making your own cleaners.

ensure-is 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:

(defform user-profile {}   :user-id [...             #(ensure-is % pos? "Invalid ID.")             ...]   :username [...]   :bio ^:red-tape/optional [...])

ensure-not passes the value through if it does not satisfy the predicate, and throws the error if it does.

red-tape.cleaners also contains some pre-made cleaners that you'll probably find useful:

(ns ...   (:require [red-tape.cleaners :as cleaners]))  (defform user-profile {}   :user-id [cleaners/to-long             cleaners/positive             ...]   :username [cleaners/non-blank              #(cleaners/length 3 20 %)              ...]   :bio ^:red-tape/optional [#(cleaners/max-length 2000 %)]   :state [cleaners/non-blank           clojure.string/upper-case           #(cleaners/choices #{"NY" "PA" "OR" ...})])

Most of the built-in cleaners take an extra argument that lets you provide a custom error message when they fail:

(defform signup-form {}   :username [#(cleaners/matches #"[a-zA-Z0-9]+" %)])  (signup-form {:username "cats and dogs!"}) ; => {...  :errors {:username "Invalid format."}  ...}  (defform better-signup-form {}   :username [#(cleaners/matches #"[a-zA-Z0-9]+" %                "Username may contain only letters and numbers.")])  (signup-form {:username "cats and dogs!"}) ; => {...  :errors {:username "Username may contain only letters and numbers."}  ...}

See the Reference section for the full list of built-in cleaners.

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 guide for more information.

```