Изменение макроса define-route в RESTAS
Существенно переработал макрос restas:define-route в RESTAS. Раньше он имел следующие аргументы:
Т.е. много-много key-параметров. Если при создании маршрута использовалось хотя бы несколько из них, то выглядело это довольно жутко. В итоге я решил отказаться от такой формы, оставил всего два key-параметра: method и content-type, а всё остальное теперь задаётся с помощью "деклараций". Декларации это специальные формы, начинающиеся с keyword-символа и расположенные в начале тела маршрута. Вот простейший пример из кода arblog:
(:render-method #'arblog.feed.tmpl:atom-feed)
(list :name *blog-name*
:href-atom (restas:gen-full-url 'posts-feed)
:href-html (restas:gen-full-url 'entry)
:posts (mapcar #'feed-post-info (ds.list-recent-posts 0 50))))
Вместо старого параметра parse-vars теперь используется декларация :sift-variables. Например поначалу я использовал такой код:
(:sift-variables (year #'parse-integer)
(month #'parse-integer)
(day #'parse-integer))
(:render-method #'render.one-post)
(ds.find-single-post year month day urlname))
Здесь указывается, что переменные year, month и day после извлечения их из переданного URL должны быть преобразованы с помощью функции parse-integer.
Однако, можно сделать намного лучше:
(:sift-variables (year 'integer)
(month '(integer :min-value 1 :max-value 12))
(day ('integer :min-value 1 :max-value 31)))
(:render-method #'render.one-post)
(ds.find-single-post year month day urlname))
Здесь не только указывается, что переменная должна быть преобразована к integer, но также задаются разумные ограничения на month и day - если переданный URL не соответствует этим ограничениям, то данный маршрут будет признан не соответствующим запрошенному URL и клиенту скорей всего (если не найдётся другого подходящего маршрута, а в arblog не найдётся) будет отправлено Not Found. Данная возможность основана на использовании библиотеки data-sift, которая пока находиться на ранней стадии развития, но что-то уже может.
Если в разных маршрутах используются одинаковые ограничения на переменные (что бывает часто), то можно избавиться от ненужного дублирования следующим образом:
(data-sift:compile-rule 'integer))
(defmethod data-sift:compile-rule ((rule (eql 'month)) &key)
(data-sift:compile-rule '(integer :min-value 1 :max-value 12)))
(defmethod data-sift:compile-rule ((rule (eql 'day)) &key)
(data-sift:compile-rule '(integer :min-value 1 :max-value 31)))
(restas:define-route one-post (":year/:month/:day/:urlname")
(:sift-variables (year 'year) (month 'month) (day 'day))
(:render-method #'render.one-post)
(ds.find-single-post year month day urlname))
Для объяснения следующего типа деклараций надо вспомнить, что ранее при компиляции маршрута создавалась функция с таким же именем и набором key-параметров. Например, объявление
(:sift-variables (year 'year) (month 'month) (day 'day))
(:render-method #'render.one-post)
(ds.find-single-post year month day urlname))
приводило к создании функции
(year (cdr (assoc :year restas:*bindings*)))
(month (cdr (assoc :month restas:*bindings*)))
(day (cdr (assoc :day restas:*bindings*)))
(urlname (cdr (assoc :urlname restas:*bindings*))))
Просто жуть. Теперь же создаваемая функция будет иметь следующие параметры:
Создание данной функции является важным для процесса разработки, поскольку позволяет тестировать код маршрутов непосредственно в REPL (если правильно задать окружение). Пример:
(arblog:one-post 2012 12 18 "ARBLOG"))
Это очень удобно, но имеет ограничение: часто для обработки маршрута необходимо извлекать дополнительную информацию из запроса, одних только параметров в URL недостаточно, а значит такие маршруты нельзя тестировать непосредственно в REPL. Для устранения данной проблемы я добавил новую возможность - декларация :additional-variables. Пример:
(:apply-render-method #'render.posts-with-tag)
(:additional-variables (skip (ignore-errors (parse-integer (hunchentoot:get-parameter "skip"))) 0))
(list tag
(ds.list-recent-posts skip *posts-on-page* :tag tag)
(navigation (restas:genurl 'posts-with-tag :tag tag)
skip
(ds.count-posts tag))))
Здесь указывается, что переменная SKIP является дополнительной переменной маршрута и должна вычислять при передаче параметров в обработчик маршрута. Как видно, здесь также можно указать значение по-умолчанию. Создаваемая при этом функция будет иметь следующие параметры:
Т.е. параметры, указанные в декларации :additional-variables, превращаются в key-параметры, которые можно указывать при экспериментах в REPL. Например:
(arblog:posts-with-tag "lisp" :skip 20))
Между прочим, если в описании маршрута указана декларация :render-method (для вызова используется funcall)) или :apply-render-method (для вызова используется apply), то вызов функции маршрута не сопряжён с генераций разметки, что существенно упрощается не только экскременты в REPL, то также и написание пресловутых unit-тестов для контролёров. Но об этом в другой раз, у меня пока в голове вертится схема с возможностью генерации Mock-объектов из спецификации указываемой в restas:define-policy, что вкупе с вышесказанным открывает первоклассные возможности для тестирования логики разрабатываемого веб-приложения.
Декларация :requirement позволяет указать дополнительные условия при проверке соответствия запроса маршруту, а :decorators используется для указания применяемых к маршруту декораторов. Вот комплексный пример:
(defmethod restas:process-route :before ((route admin-route) bindings)
(multiple-value-bind (user password) (hunchentoot:authorization)
(unless (ds.check-admin user password)
(hunchentoot:require-authorization))))
(defun @admin (route)
(make-instance 'admin-route :target route))
(restas:define-route admin-preview-create-post ("admin/create-post" :method :post)
(:requirement (hunchentoot:post-parameter "preview"))
(:decorators '@admin)
(:apply-render-method #'render.admin-edit-post)
(:additional-variables (markup (hunchentoot:post-parameter "content"))
(title (hunchentoot:post-parameter "title"))
(tags (post-parameter-tags)))
(list :title title
:markup markup
:tags tags
:preview (markup.render-content markup)))
В данный момент я использую arblog в качестве тестовой площадки, на которой обкатываю новые решения. Цель - привести исходный код arblog к некому идеальному состоянию за счёт доработки RESTAS, а затем переключиться на что-нибудь более сложное, например на lisper.ru. Соответственно, arblog зависит от самых свежих версий RESTAS, data-sift и cl-closure-template.