пятница, 21 января 2011 г.

Автоматическая генерация ссылок в RESTAS

Одной из типовых проблем веб-разработки является генерация ссылок. В некоторых простых случаях на неё можно просто закрыть глаза и создавать ссылки "вручную". Например, недавно rigidus опубликовал на хабре статью, в которой рассказал про создание простого сайта на Common Lisp (с использование RESTAS). В данном примере для генерации главного меню используется такой код:
(defun menu ()
(list (list :link "/" :title "Главная")
(list :link "/about" :title "About")
(list :link "/articles" :title "Статьи")
(list :link "/resourses" :title "Ресурсы")
(list :link "/contacts" :title "Контакты")))
Т.е. ссылки жёстко задаются в теле программы. Поскольку здесь их всего 5 и они очень простые, то в данном случае это не создаёт больших проблем.

Однако, по мере роста приложения, а также в процессе изменения его структуры, проблема сохранения актуальности ссылок приобретает серьёзный характер.

Самый лучший способ решения данной проблемы использовать автоматическую генерацию ссылок. В RESTAS для этого есть специальная поддержка на базе функции restas:genurl. Например, с использованием данной функции вышеприведённый код можно было бы переписать следующим образом:
(defparameter *mainmenu*
'((main . "Главная")
(about . "About")
(articles . "Статьи")
(resources . "Ресурсы")
(contacts . "Контакты")))

(defun menu ()
(iter (for (route . title) in *mainmenu*)
(collect (list :link (restas:genurl route)
:title title))))
Здесь для генерации ссылок используется символ, связанный с конкретным маршрутом, а получающиеся ссылки будут учитывать базовый url, по которому подключается разрабатываемый модуль.

Это очень простая ситуация, которая решается совершенно тривиальным образом. На сайте lisper.ru имеет место более сложный случай. Исходный код данного ресурса разбит на несколько совершенно независимых пакетов, которые объединяются в один сайт на основе механизма модулей. Простое использование restas:genurl здесь не подходит, поскольку маршруты, на которые ссылается главное меню, находятся в разных модулях. Для определения состава главного меню используется такое объявление:
(defparameter *mainmenu* `(("Главная" nil main)
("Статьи" rulisp-articles restas.wiki:main-wiki-page)
("Планета" rulisp-planet restas.planet:planet-main)
("Форум" rulisp-forum restas.forum:list-forums)
("Сервисы" nil tools-list)
("Practical Common Lisp" rulisp-pcl rulisp.pcl:pcl-main)
("Wiki" rulisp-wiki restas.wiki:main-wiki-page)
("Файлы" rulisp-files restas.directory-publisher:route :path "")
("Поиск" nil google-search)))
Здесь каждому элементу меню соответствует список, содержащий следующие элементы: заголовок, субмодуль (символ, который используется при вызове restas:mount-submodule), символ маршрута (указанный в restas:define-route) и возможно несколько ключевых параметров (параметров маршрута). А для непосредственной генерации ссылок используется такой код:
(in-package #:rulisp)

(restas:with-submodule (restas:find-upper-submodule #.*package*)
(iter (for item in *mainmenu*)
(collect (list :href (apply #'restas:genurl-submodule
(second item)
(if (cdddr item)
(cddr item)
(last item)))
:name (first item)))))
Наиболее интересной в данном коде является строка
(restas:with-submodule (restas:find-upper-submodule #.*package*)
Дело в том, что генерация меню происходит каждый раз при генерации HTML-страницы для всех маршрутов, которые находятся в разных модулях и имеют различный контекст выполнения, а параметр *mainmenu* составлен с точки зрения самого верхнего модуля :rulisp, который используется для запуска приложения с помощью start.

Структура субмодулей в RESTAS образует иерархию и restas:find-upper-submodule позволяет найти нужный модуль выше по дереву, а макрос restas:with-submodule выполнить код в контексте найденного модуля. Таким образом, генерация ссылок работает всегда одинаково, не зависимо от контекста выполнения этого кода.

restas:find-upper-submodule и restas:with-submodule я добавил только сегодня, так что они пока есть только в git-версии RESTAS.