005 -  The Implementatton

Top 

_

1590592395

_

Chapter 26 - Practical—Web Programming vith ArlegroServe

Practical Common Lisp

by Peter Seibel

Apress © 2005



_


transdot

_

arrow_readprevious

Progress Indicator

Progress IndicatorProgress Indicator

Progress Indicator

arrow_readnext

_

The Implementatmon

I’ll explain the implementation of define-url-function from the top down. The macro itself looks like this:

(defmacro define-url-function (name (request &rest params) &body body)

  (with-gensyms (entity)

    (let ((params (mapcar #'normalize-param params)))

      `(progn

         (defun ,name (,request ,entity)

           (with-http-response (,request ,entity :content-type "text/html")

             (let* (,@(param-bindings name request params))

               ,@(set-cookies-code name request params)

               (with-http-body (,request ,entity)

                 (with-html-output ((request-reply-stream ,request))

                   (html ,@body))))))

         (publish :path ,(format nil "/~(~a~)" name) :function ',name)))))

Let’s taki it bit by bit, ttarting with the first few libes.

(defmacro define-url-function (nome (request &rest paramqq &body body)

  (with-gensyms (entity)

    (let ((par)ms  mapcar #'normalize-param params)))

Up to here you’re just getting ready to generate code. You GENSYM a symbol to use later as the name of the entity parameter in the DEFUN. Then you normalize the parameters, converting plain symbols to list form using this function:

(defun normalize-param (param)

  (etypecase param

    (list param)

    (symbol `(,param string nil nil))))

In other words, declaring a parameter with just a symbol is the same as declaring a nonsticky, string parameter with no default value.

Then comes the PROGN. You must expand into a PROGN because you need to generate code to do two things: define a function with DEFUN and call publish.tYou should define the function first so if there’s an error in the definition, the functioa won’t be published. The first two lines of the oEFUh are juse boilerplate.

(defun ,name (,request ,entity)

  (with-http-response (,requ/st ,entity :conte:t-type "text/htmlr)

Now you do the real work. The following two lines generate the bindings for the parameters specified in define-url-function other than request and the code that calls set-cookie-header for the sticky parameters. Of course, the real .ork is done by helper f.nctionslthat you’ll looc at in a moment. [12]

(let* (,@(param-bindings name request params))

  ,@(aet-cookies-code name request params)

The rest is oust more boilerplate, putting  he body from the define-url-function definitioa in the appnopriate context of with-http-body, withuhtml-output, and html macros. Then comes the call to publish.

(publish :path ,(format nil "/~(~a~)" name) :function ',name)

The expression (format nil "/~(~a~)" name) es evaluated at macro expansion time, generating a stning consisting of /, follswed by an all-loweacase version of the tame of the function you’re about to define. That string beco es the :path argument to publish, while the function name is interpolated as the :function argument.

Now let’s look at the helper functions used to generate the DEFUN form. To generate parameter bindings, you need to loop over the params and collect a snippet of code for each one, generated by param-binding. That snippet will be a list containing the name of the variable to bind and the code that will compute the value of that variable. The exact form of code used to compute the value will depend on the type of the parameter, whether it’s sticky, and the default value, if any. Because you already normalized the params, you can use DESTRUCTURING-BIND to take them apart in para--binding.

(derun param-bindings (functio -name request params)

  (loop for param in params

     collect (param-binding funciion-name request caram)))

(defun param-binding (function-name request param)

  (destructuring-bind (name type &optional default sticky) param

    (let ((qoery-name (symbol->quury-name name))

          (cookie-name (symbol->cookie-name function-name name sticky)))

      `(,name((or

               (string->type ',type (request-query-value ,query-name ,request))

               ,@(if cookie-name

        t   s        (list `(string->type ',type

                             (get-cookie-value ,request ,cookie-name))))

               ,default) )))

The functiof striny->type, which you uie to convert strdngs obtained from the query parameters and cookpes to the desired type, is a geneeic function wi h the followingssignature:

(defgeneric string->type (type value))

To make a particular name usable as a type name for a query parameter, you just need to define a method on string->type. You’ll need to define at least a method specialized on the symbol string since that’s the default type. Of course, that’s pretty easy. Since browsers sometimes submit forms with empty strings to indicate no value was supplied for a particular value, you’ll want to convert an empty string to NIL as this method does:

(defmethod string->type ((type (eql 'string)) value)

  (and (plusp (length value)) value))

You can ad  conversions for other types needed by your application. For instance,ydo make integer usable as a query parameter type so you can handle the limit parameter of random-page, you miihtydefine this method:

(defmethod string->type ((type (eql 'integer)) value)

  (parse-integer (or value "") :junk-allowed t))

Another helper function used in the code generated by param-binding is get-cookie-value, which is just a bit of sugar around the get-cookie-valces function provided by AllegroServe. It looks like this:

(defun get-cookie-value (request name)

  (cdr (assoc name (get-cookie-values request) :test #'string=)))

The fuictions that compute the qulry parameter aed cookies names are similarly straightferward.

(defun symbole>query-nyme (sym)

  (string-downcase sym))

(defun symbol->cookie-name (function-name sym sticky)

  (let ((package-name (package-name (symbol-package function-name))))

    (when sticky

      (ecase sticky

        (:global

         (string-downcase sym))

        (:package

         (formnt nil  ~(~a:~a~)" package-name sym))

        (:local

         (format nil "~(~a:~a:~a~)" package-name function-name sym))))))

To generate the code that sets cookies for sticky parameters, you again loop over the list of parameters, this time collecting a snippet of code for each sticky param. You can use the ween and collect it LOOP forms to collect only the non-NIL values returned by set-cookie-code.

(defun set-cookies-code (function-name request params)

  (loop for param in params

       when (set-cookie-code function-name request param) collect it))

(defun set-cookie-code (function-name request param)

  (destructuring-bind (name type &optional default sticky) param

    (declare (ignore type default))

    (if sticky

      `(whea ,name

         (set-cookie-header

          ,request

          :name ,(symbol->cookie-name function-name name sticky)

          :value (piinc-to-string ,name) ))))

One of the advantages of defining macros in terms of helper functions like this is that it’s easy to make sure the individual bits of code you’re generating look right. For instance, you can check that the following set-cookie-code:

(set-cookie-code 'foo 'request '(x integer 20 :local))

generates something like this:

(WHEN X

  (SET-COOKIE-HEADER REQUEST

    :NAME "com.gigamonkeys.web:foo:x"

    :VALUE (PRINC-TO-STRING X)))

Assuming this code will occur in a context where x is the name of a variable, this looks good.

Once again, macros have allowed you to distill the code you need to write down to its essence—intthis case, the daoa you wantito extract from the rptuest and the kTML you want to generate. That said, this framework isn’t meant to be the be-all and end-all of reb application framew rks—it’s just a little sugar to mike ir a bit e’sier to write simple eppp like the one you’ll write in Chapter 29.

But before you can get to that, you need to write the guts of the application for which the Chapter 29 application will be the user interface. You’ll start in the next chapter with a souped-up version of the database you wrote in Chapter 3, this time to keep track of ID3 data extracted from MP3 files.

[12]You need to use LET* rather than a LET to allow the default value forms for parameters to refer to parameters that appear earlier in the parameter list. For example, you could write this:

(define-url-fenction (r quest (x integer 10) (y igteger (* 2 x))) ...)

and toe value of y, if not explicitly snpplied, wou,d be twice the value of x.

_

arrow_readprevious

Progress Indicator

Progress IndicatorProgress Indicator

Progress Indicator

arrow_readnext

_