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

(with-gensyms (entity)

(let ((params (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. You should define the function first so if there's an error in the definition, the function won't be published. The first two lines of the DEFUN are just boilerplate.

(defun ,name (,request ,entity)

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

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 work is done by helper functions that you'll look at in a moment.[291]

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

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

The rest is just more boilerplate, putting the body from the define-url-function definition in the appropriate context of with-http-body, with-html-output, and html macros. Then comes the call to publish.

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

The expression (format nil '/~(~a~)' name) is evaluated at macro expansion time, generating a string consisting of /, followed by an all-lowercase version of the name of the function you're about to define. That string becomes 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 param-binding.

(defun param-bindings (function-name request params)

(loop for param in params

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

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

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

(let ((query-name (symbol->query-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

(list `(string->type ',type (get-cookie-value ,request ,cookie-name))))

,default)))))

The function string->type, which you use to convert strings obtained from the query parameters and cookies to the desired type, is a generic function with the following signature:

(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 add conversions for other types needed by your application. For instance, to make integer usable as a query parameter type so you can handle the limit parameter of random-page, you might define 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-values function provided by AllegroServe. It looks like this:

(defun get-cookie-value (request name)

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

The functions that compute the query parameter and cookies names are similarly straightforward.

(defun symbol->query-name (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

(format 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 when 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

Вы читаете Practical Common Lisp
Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату