Fork me on GitHub

RESTAS

Фрэймворк для разработки web-приложений на Common Lisp

Модули

Module

С точки зрения Common Lisp, модуль -- это просто пакет(package). С точки зрения RESTAS модуль -- это набор маршрутов , определяющих структуру web-приложения. Модуль создаётся с помощью макроса restas:define-module, который создаёт пакет с соответствующим именем, а ещё проводит некоторую дополнительную инициализацию этого пакета. Например:

(restas:define-module #:hello-world
  (:use #:cl)
)

Теперь можно создать в этом модуле несколько маршрутов:

(in-package #:hello-world)

(restas:define-route main ("hello")
  "<h1>Hello world!</h1>"
)

Обращаю внимание на принципиальную важность раскрытия макроса restas:define-route в пакете (после in-package), связанном с определённым модулем.

И наконец, полученный модуль можно запустить как web-сайт:

(restas:start '#:hello-world :port 8080)

В функцию restas:start можно также передать ключевой параметр :hostname, что позволяет обслуживать несколько виртуальных хостов в рамках одного процесса.

Таким образом, после размешения в модуле нескольких маршрутов, модуль можно использовать для запуска web-приложения. Но у модулей есть ещё и другой вариант использования.

Подмодули

Модули предлагают интересный подход к повторному использованию кода при разработке web-приложений. Отличие web-компонента от простой библиотеки, содержащей функции, макросы, классы и т.п в том, что web-компонент также должен содержать информацию об обслуживаемых им url, и эту информацию надо использовать в механизме диспетчеризации запросов, правильно определяя код, ответственный за обработку поступившего запроса. В терминах routes (маршрутов) любой повторно используемый web-компонент является просто списком обрабатываемых им маршрутов, а это как раз и есть то, чем являются модули с точки зрения RESTAS. Для повторного использования модуля из другого модуля используется макрос restas:mount-submodule. Пример (зависит от кода выше):

(restas:define-module #:test
  (:use #:cl)
)


(in-package #:test)

(restas:mount-submodule test-hello-world (#:hello-world))

(restas:start '#:test :port 8080)

В данном примере определяется новый модуль test, к нему присоединяется определённый выше модуль hello-world (а получившийся подмодуль ассоциируется с символом test-hello-world) и модуль test запускается на порту 8080. Хотя в самом модуле test не определён ни один маршрут, но в итоге он способен обрабатывать запросы, поступающие на /hello благодаря включению в себя модуля hello-world.

В таком виде данный функционал не выглядит очень полезным и вот почему. Для успешного повторного использования любого компонента надо уметь его конфигурировать, настраивать его параметры - без этой возможности повторное использование сведётся к технике copy/paste с последующим редактированием кода, что выглядит удручающее само по себе, а в контексте CL ещё имеет и множество технических ограничений (что связано с тем, что понятие пакета никак не связано с физическим размещением кода на файловой системе). В ООП традиционным способом решения проблем конфигурации является использование классов, но, хотя Common Lisp и имеет сверхмощную поддержку ООП (CLOS + MOP), я решил всё-таки отказаться от подобного подхода: возникающие проблемы дизайна, проектирования, бесконечные интерфейсы и наследование всегда существенным образом повышают уровень сложности системы, что кажется мне совершенно излишним для такой простой области, как разработка web-приложений. Для решения этой проблемы в Common Lisp есть ещё один потрясающий механизм: динамические переменные. Вот пример настоящего кода, используемого на lisper.ru для публикации статических файлов:

(restas:mount-submodule rulisp-static (#:restas.directory-publisher)
  (restas.directory-publisher:*directory* (merge-pathnames "static/" *resources-dir*))
  (restas.directory-publisher:*autoindex* nil)
)

В данном примере используется модуль restas-directory-publisher.

В модуле определены (с помощью defparameter или defvar) несколько глобальных динамических переменных, которые можно использоваться для настройки его работы. В макросе некоторые из этих переменных связываются c новыми значения, но эти связывания не применяются непосредственно, а сохраняются в виде контекста для использования в будущем. При обработке запроса диспетчер находит маршрут, определяет связанный с ним подмодуль, настраивает окружение на основе сохранённого контекста и производит дальнейшую обработку запроса в рамках этого окружения (для этого используется вызов progv). Данный механизм напоминает Buffer-Local Variables в Emacs.

При определении нового модуля с помощью restas:define-module в него (в package) добавляется переменная *baseurl*, которая должна быть списком строк и определяет базовый url, по которому будет активизирован данный модуль, по-умолчанию она установленная в nil. Данную переменную можно использовать в restas:mount-submodule для задания url, по которому будет подключён подмодуль. Вот более сложный пример использования модуля restas-directory-publisher на сайте lisper.ru (посмотреть этот код в работе можно по адресу http://lisper.ru/files/):

(restas:mount-submodule rulisp-files (#:restas.directory-publisher)
  (restas.directory-publisher:*baseurl* '("files"))
  (restas.directory-publisher:*directory* (merge-pathnames "files/" *vardir*))
  (restas.directory-publisher:*autoindex-template*
   (lambda (data)
     (rulisp-finalize-page :title (getf data :title)
                           :css '("style.css" "autoindex.css")
                           :content (restas.directory-publisher.view:autoindex-content data)
)
)
)
)

Кстати, вкупе с предыдущим, данный пример показывает двукратное использование одного модуля с различными режимами работы в рамках одного и того же сайта, без каких-либо конфликтов между собой.

Внутренняя инициализация

Макрос restas:mount-submodule позволяет контролировать настройку модуля "снаружи", но порой надо иметь возможность влиять на создаваемый контекст изнутри модуля. Например, модуль restas-planet, который используется для организации Russian Lisp Planet нуждается в механизме для сохранения объекта-робота (он по заданному расписанию считывает ленты и объединяет их в одну), который должен быть вычислен на основе переменных, которые могут быть помещены в контекст submodule. Для такого случая предусмотрена generic-функция restas:define-initialization, вот реальный код из planet.lisp:

(defmethod restas:initialize-module-instance :before ((module (eql #.*package*)) context)
  (restas:with-context context
    (when *feeds*
      (restas:context-add-variable
        context
        '*spider*
        (make-instance 'spider
                       :feeds *feeds*
                       :schedule *schedule*
                       :cache-dir (if *cache-dir*
                                      (ensure-directories-exist
                                        (merge-pathnames "spider/" *cache-dir*)
)
)
)
)
)
)
)

Здесь производится вычисление объекта spider, который ассоциируется с переменной *spider* и помещается в контекст создаваемого submodule. Данный код будет вычислен при вычислении формы restas:mount-submodule. Поскольку создание объекта spider приводит к запуску планировщика (в частности, создаётся таймер), то также надо уметь останавливать их при повторном вычислении формы restas:mount-submodule, для этого предусмотрена generic-функция finalize-module-instance

(defmethod restas:finalize-module-instance :after ((module (eql #.*package*)) context)
  (let ((spider (restas:context-symbol-value context '*spider*)))
    (when spider
      (spider-stop-scheduler spider)
)
)
)

Дуализм

Описанная схема подразумевает дуализм: модуль как standalone-приложение, и он же как компонент повторного использования, что позволяет разрабатывать модуль без какого-либо учёта возможности повторного использования, а потом минимальной ценой (просто приводя его к "правильному" дизайну) превращать в многократно используемый компонент. Подобный подход, как мне кажется, позволяет в значительной степени избежать проблем, свойственных традиционному ООП-дизайну.

В качестве демонстрации, вот код для запуска restas-directory-publisher, который я использовал выше как повторно используемый компонент, в виде standalone-приложения:

(restas:start '#:restas.directory-publisher
              :port 8080
              :context (restas:make-context (restas.directory-publisher:*baseurl* '("tmp"))
                                            (restas.directory-publisher:*directory* #P"/tmp/")
                                            (restas.directory-publisher:*autoindex* t)
)
)

Теперь открыв в браузере страницу http://localhost:8080/tmp/ можно будет наблюдать содержимое директории #P"/tmp/".

@2009-2011 Moskvitin Andrey