Commits

Steve Losh  committed a58c791

Start documenting.

  • Participants
  • Parent commits aedbc19

Comments (0)

Files changed (13)

 classes/
 checkouts/
 pom.xml
+docs/build
+feedback
 *.jar
 *.class
 .lein-deps-sum

File docs/01-installation.markdown

+Installation
+============
+
+Red Tape requires Leiningen 2.  Add the following to your `project.clj`:
+
+    [red-tape "0.0.1"]
+
+That's all there is to it.  Red Tape depends on one other library (Slingshot)
+but Leiningen should handle that for you.

File docs/02-basics.markdown

+Basics
+======
+
+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.
+
+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.
+
+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.
+
+[TOC]
+
+Scaffolding
+-----------
+
+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:
+
+    :::clojure
+    (ns feedback
+      (:require [compojure.core :refer :all]
+                [compojure.route :as route]
+                [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 save-feedback [from comment]
+      ; In a real app this would save the string to a database, email
+      ; it to someone, etc.
+      (println from "said:" comment))
+
+    (defn handle-get []
+      ; ...
+    )
+
+    (defn handle-post []
+      ; ...
+    )
+
+    (defroutes app
+      (GET  "/" request (handle-get request))
+      (POST "/" request (handle-post request)))
+
+    (def handler (-> app
+                   wrap-params))
+
+    (defonce server
+      (run-jetty #'handler {:port 3000}))
+
+That's about it for the boilerplate.  The next step is to fill in the bodies of
+`handle-get` and `handle-post`.  We'll need to do a few things:
+
+* Validate incoming POST data to make sure it's sane.
+* If the data isn't valid, we need to inform the user and re-render the form
+  nicely.
+* Once we've got valid data, we'll clean it up and send it off to be saved.
+
+This is where Red Tape comes in.
+
+Defining the Form
+-----------------
+
+The main part of Red Tape is the `defform` macro.  Let's define a simple
+feedback form:
+
+    :::clojure
+    (ns feedback
+      ; ...
+      (require [red-tape.core :refer [defform]]))
+
+    (defform feedback-form {}
+      :name []
+      :comment [])
+
+`defform` 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 *use* the form we've defined.
+
+Using the Form
+--------------
+
+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:
+
+    :::clojure
+    (defn handle-get
+      ([request]
+        (handle-get request (feedback-form)))
+      ([request form]
+        page))
+
+There are a couple of things going on here.
+
+We've split the definition of `handle-get` 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.
+
+Calling `(feedback-form)` without data returns a map representing a fresh form.
+It will look like this:
+
+    :::clojure
+    {:fresh true
+     :valid false
+     :arguments {}
+     :data {}
+     :results nil
+     :errors nil}
+
+We'll see how to use this later.  Let's move on to `handle-post`:
+
+    :::clojure
+    (defn handle-post [request]
+      (let [data (:params request)
+            form (feedback-form data)]
+        ; ...))
+
+`handle-post` takes the raw HTTP POST data (from `(:params request)`) 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:
+
+    :::clojure
+    {:fresh false
+     :valid true
+     :arguments {}
+     :data {:name "Steve"
+            :comment "Hello!"}
+     :results {:name "Steve"
+               :comment "Hello!"}
+     :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.
+
+Let's add a bit of data cleaning to the form to get something more useful.
+
+Cleaners
+--------
+
+Every field you define in a `defform` also gets a vector of "cleaners"
+associated with it.  A cleaner is simply a vanilla Clojure function that takes
+one argument (the incoming value) and returns a new value (the outgoing result).
+
+Let's see this in action by modifying our form to strip leading and trailing
+whitespace from the user's name automatically:
+
+    :::clojure
+    (defform feedback-form {}
+      :name [clojure.string/trim]
+      :comment [])
+
+`clojure.string/trim` is just a normal Clojure function that trims off
+whitespace.  Let's imagine that the user entered " Steve " as their name this
+time.  Calling `(feedback-form data)` now results in the following map:
+
+    :::clojure
+    {:fresh false
+     :valid true
+     :arguments {}
+     :data    {:name " Steve " :comment "Hello!"}
+     :results {:name "Steve"   :comment "Hello!"}
+     :errors nil}
+
+The `:data` in the result map still contains the raw data the user entered, but
+the `:results` have had their values passed through their cleaners first.
+
+You can define as many cleaners as you want for each field.  The data will be
+threaded through them in order, much like the `->` macro.  This lets you define
+simple cleaning functions and combine them as needed.  For example:
+
+    :::clojure
+    (defform feedback-form {}
+      :name [clojure.string/trim
+             clojure.string/lower-case]
+      :comment [clojure.string/trim])
+
+    (feedback-form {:name " Steve " :comment " Hello! "})
+    ; =>
+    {:fresh false
+     :valid true
+     :data    {:name " Steve " :comment " Hello! "}
+     :results {:name "steve"   :comment "Hello!"}
+     ; ...
+     }
+
+Here we're trimming the name and then lowercasing it, and trimming the comment
+as well (but not lowercasing it).
+
+Validation
+----------
+
+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.
+
+Let's look at an example:
+
+    :::clojure
+    (defform age-form
+      :age [clojure.string/trim
+            #(Long. %)])
+
+If we call this form with a number, everything is fine:
+
+    :::clojure
+    (age-form {:age "27"})
+    ; =>
+    {:fresh false
+     :valid true
+     :data    {:age "27"}
+     :results {:age 27}
+     :errors nil}
+
+But if we try to feed it garbage:
+
+    :::clojure
+    (age-form {:age "cats"})
+    ; =>
+    {:fresh false
+     :valid false
+     :data {:age "cats"}
+     :results nil
+     :errors {:age <NumberFormatException: ...>}}
+
+There are a few things to see here.  If any cleaner function throws an
+Exception, the resulting map will have `:valid` set to `false`.
+
+There will also be no `:results` entry in an invalid result.  You only get
+`:results` if your entire form is valid.
+
+The `:errors` 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.
+
+Red Tape uses Slingshot's `try+` to catch exceptions, so if you want you can use
+`throw+` to throw errors in an easier-to-manage way and they'll be caught just
+fine.  We'll see an example of this later.
+
+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.
+
+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:
+
+    :::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>
+    ")
+
+The only change here is the `value='%s'` bits, which we'll use to stick in some
+data later.
+
+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:
+
+    :::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))))
+
+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:
+
+    :::clojure
+    (defn handle-post [request]
+      (let [data (:params request)
+            form (feedback-form data)]
+        (if (:valid form)
+          (let [{:keys [name comment]} (:results form)]
+            (save-feedback name comment)
+            (redirect "/"))
+          (handle-get request form))))
+
+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 `:results` and we're done.
+
+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.
+
+Summary
+-------
+
+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:
+
+* Defining the form.
+* Defining a GET handler that creates a blank form.
+* Defining a GET handler that takes a form (either blank or invalid) and renders
+  it to HTML.
+* 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.
+
+Now that you've got the general idea, it's time to look at a few topics in more
+detail.

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/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/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/07-reference.markdown

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

File docs/footer.markdown

+Crafted with love by [Steve Losh](http://stevelosh.com).
+Documentation built with [d](http://sjl.bitbucket.org/d/).
+
+<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>
+
+<script type="text/javascript">
+  var _gauges = _gauges || [];
+  (function() {
+    var t   = document.createElement('script');
+    t.type  = 'text/javascript';
+    t.async = true;
+    t.id    = 'gauges-tracker';
+    t.setAttribute('data-site-id', '4f80ace2f5a1f538860000c2');
+    t.src = '//secure.gaug.es/track.js';
+    var s = document.getElementsByTagName('script')[0];
+    s.parentNode.insertBefore(t, s);
+  })();
+</script>

File docs/index.markdown

+Red Tape is a Clojure library for processing form data.  It's heavily inspired
+by Django's forms, with some Clojurey twists.
+
+**License:** MIT/X11  
+**Documentation:** <http://sjl.bitbucket.org/red-tape/>  
+**Issues:** <http://github.com/sjl/red-tape/issues/>  
+**Mercurial:** <http://bitbucket.org/sjl/red-tape/>  
+**Git:** <http://github.com/sjl/red-tape/>
+
+Read the full documentation to learn how Red Tape can make processing forms safe
+and painless.
+Red Tape

File src/red_tape/core.clj

   or (defn name ...).
 
   "
-  [{:keys [bindings initial clean]} fields]
-  (let [initial (or initial {})
-        bindings (or bindings [])
-        binding-keys (map keyword bindings)
+  [{:keys [arguments initial clean] :or {initial {} arguments []}} fields]
+  (let [arg-keys (map keyword arguments)
 
         ; Create the binding map, which is a map of keywords to symbols:
         ;
         ; This will end up being the body for (part of) the form function:
         ;
         ; (defn foo-form [f1 f2]
-        ;   {:bindings {:f1 f1 :f2 f2}})
-        binding-map (zip-map binding-keys bindings)
+        ;   {... :arguments {:f1 f1 :f2 f2} ...})
+        binding-map (zip-map arg-keys arguments)
 
         ; Transform fields from:
         ;
-        ; [:f1 [a] :f2 [b]]
+        ; [:f1 [a] :f2 [b c]]
         ;
         ; into vector pairs like:
         ;
         ; [[:f1 [a]]
-        ;  [:f2 [b]]]
+        ;  [:f2 [b c]]]
         fields (mapv vec (partition 2 fields))
 
         ; Get a vector of just the field keys like [:f1 :f2].
         field-keys (mapv first fields)
 
-        ; A fresh, unbound form simply returns a map.
-        fresh-body `{:fresh true
-                     :bindings {}
-                     :data (initial-data ~field-keys ~initial)
-                     :valid nil
-                     :errors nil
-                     :results nil}
-
-        ; A fresh, bound form includes the bindings in the map.
-        fresh-body-bound (assoc fresh-body :bindings binding-map)]
-    (remove nil?
-            [`([] ~fresh-body)
-             (when-not (empty? bindings)
-               `([~@bindings] ~fresh-body-bound))
-             `([~@bindings data#]
-               (-> ~fields
-                 (zip-fields data#)
-                 process-fields
-                 (process-result ~clean)
-                 (assoc :fresh false
-                        :data data#
-                        :bindings ~binding-map)))])))
+        ; A fresh form simply returns a map.
+        fresh `{:fresh true
+                :arguments ~binding-map
+                :data (initial-data ~field-keys ~initial)
+                :valid nil
+                :errors nil
+                :results nil}]
+    `[([~@arguments]
+       ~fresh)
+      ([~@arguments data#]
+       (-> ~fields
+         (zip-fields data#)
+         process-fields
+         (process-result ~clean)
+         (assoc :fresh false
+                :data data#
+                :arguments ~binding-map)))]))
 
 
 (defmacro form
-  [{:keys [bindings initial clean] :as options} & fields]
+  [{:keys [arguments initial clean] :as options} & fields]
   `(fn ~@(form-guts options fields)))
 
 (defmacro defform
-  [form-name {:keys [bindings initial clean] :as options} & fields]
+  [form-name {:keys [arguments initial clean] :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]))
 
 
 (defform number-form {}
 (defform stripping-number-form {}
   :n [clojure.string/trim cs/to-long])
 
-(defform state-form {:bindings [states]}
+(defform state-form {:arguments [states]}
   :state [clojure.string/trim (partial cs/choices states)])