пятница, 29 октября 2010 г.

RESTAS и специальные страницы

Если в обработчике запроса для сервера Hunchentoot выставляется код возврата в какой-либо из кодов ошибок, то Hunchentoot игнорирует возвращаемое диспетчером значение и самостоятельно формирует соответствующую стандартную специальную страницу. Такое поведение не всегда является приемлемым. Изменить его можно с помощью переменной hunchentoot:*handle-http-errors-p* - если она выставлена в NIL, то вся ответственность за формирование специальных страниц ложится на пользовательский код. Однако, изменять её глобально (например, при загрузке системы) тоже не очень хорошо (потому что глобально). На самом деле я бы хотел, что бы пользовательский код смог указать будет ли он сам формировать специальную страницу или же пусть Hunchentoot генерирует стандартную. Самое простое решение - изменять значение переменной hunchentoot:*handle-http-errors-p* в пользовательском коде, но так, что бы эти изменения происходили не глобально, а только в контексте обрабатываемого запроса. Превратить глобальную переменную hunchentoot:*handle-http-errors-p* в "более локальную" очень просто, для RESTAS я сделал это так:
(defmethod hunchentoot:process-request ((request restas-request))
(let ((hunchentoot:*handle-http-errors-p* hunchentoot:*handle-http-errors-p*))
(call-next-method)))
Теперь можно смело менять значение переменной hunchentoot:*handle-http-errors-p* без какого-либо страха что это отразится на функционировании всей системы.

Если обработчик маршрута в RESTAS возвращает целое число, то оно интерпретируется как код возврата, за что отвечает соответствующая специализация метода restas:render-object. Что бы упростить определение для модулей своих способов формирования специальных страниц я переписал эту специализацию следующим образом:
(defmethod render-object :before (designer (code integer))
(setf hunchentoot:*handle-http-errors-p* nil
(hunchentoot:return-code*) code))

(defmethod render-object (designer (code integer))
"Default handler for HTTP status code"
(declare (ignore designer))
(setf hunchentoot:*handle-http-errors-p* t))
Это реализация по-умолчанию, которая в :before методе выставляет код возврата и присваивает переменной hunchentoot:*handle-http-errors-p* значение NIL, а в основном методе присваивает переменной hunchentoot:*handle-http-errors-p* значение T. Таким образом, по-умолчанию при возврате из маршрута целого числа за формирование специальной страницы будет отвечать Hunchentoot. Но, определив свой тип designer (и присвоив объект этого типа переменной модуля *default-render-method*) можно изменить это поведение и формировать специальные страницы в пользовательском коде. Очень удобно, что при этом также можно использовать и eql-специализаторы, например:
(defmethod restas:render-object ((designer mydrawer) (code (eql hunchentoot:+http-internal-server-error+)))
(setf (hunchentoot:content-type*) "text/plain")
"Шеф, всё очень плохо!")
При этом, может случить так, что если маршрут возвращает hunchentoot:+http-not-found+, то будет формироваться "красивое" сообщение о том, что на данном ресурсе такой страницы нет, но если RESTAS просто не найдёт подходящего маршрута, то будет отдаваться стандартная страница Hunchentoot. Для решение данной проблемы проще всего определить "универсальный" маршрут, который будет проверяться в последнюю очередь:
(restas:define-route not-found ("*any")
hunchentoot:+http-not-found+)
Описанный подход (который можно использовать с git-версией RESTAS) хорошо подходит для обработки специальных страниц общим для всего модуля образом. Но сейчас у меня есть один модуль, который предоставляет некоторое API для клиентского кода. Если маршрут в этом модуле отрабатывает успешно, то клиенту возвращаются данные в формате JSON. А в случае ошибки я бы хотел возвращать код возврата hunchentoot:+http-internal-server-error+ и прикреплять к нему сообщение, описывающие тип ошибки. Для упрощения реализации данного функционала я ввёл следующую функцию:
(defun abort-route-handler (obj &key return-code content-type) ..)
Эта функция немедленно прекращает обработку маршрута, а obj передаётся в restas:render-object для формирования ответа. Тонкость в том, что если в данную функцию передан return-code, то значение переменной hunchentoot:*handle-http-errors-p* выставляется в NIL. Теперь, вызов
(restas:abort-route-handler "Очень плохие данные!"
:return-code hunchentoot:+http-internal-server-error+
:content-type "text/plain")
отправит клиенту ответ с кодом 500 и сообщением "Очень плохие данные!".

понедельник, 25 октября 2010 г.

RESTAS-0.1

Оформил релиз RESTAS-0.1, скачать можно здесь, посмотреть документацию здесь, задавать вопросы на русском можно здесь, а на английском здесь (там сейчас вообще пусто).

Установка через мой форк clbuild больше не поддерживается, ну его.

среда, 20 октября 2010 г.

Изменения в RESTAS

Провёл два изменения в RESTAS.

Первое связано с механизмом субмодулей. При использовании макроса restas:mount-submodule можно настроить переменные модуля, но статическим образом, т.е. эти настройка никак не связаны с настройками родительского модуля. Порой это очень не удобно. Теперь, после проведённых изменений, можно делать примерно так:
(restas:mount-submodule -publisher- (#:restas.directory-publisher))

(restas:define-initialization (context)
(restas:with-context context
(setf (restas:context-symbol-value (restas:submodule-context (restas:find-submodule '-publisher-))
'restas.directory-publisher:*directory*)
*restas-documentation-dir*)))
Здесь переменной restas.directory-publisher:*directory* модуля restas.directory-publisher присваивается значение *restas-directory-dir* из модуля, к которому монтируется публишер и происходит это в момент инициализации основного модуля. Данный код взят из файла doc.lisp.


Второе изменение состоит в том, что я удалил поддержку garbage-pools и таким образом переменная restas:*request-pool* больше не доступна. Это сделано для упрощения, уменьшения зависимостей и для решения проблемы с LispWorks.

четверг, 14 октября 2010 г.

SLIME на службе автоматизации

В предыдущем посте я рассказал как можно ускорить компиляцию шаблонов cl-closure-template и тем самым упростить процесс разработки. Кроме того, мне также досаждает другая проблема. Для перекомпиляции этих шаблонов я обычно переключаюсь в файл, в котором расположен код компилирующий шаблоны, ставлю курсов на этот код и нажимаю M-C-x. Другой вариант - оформить это в виде функции и вызывать через REPL. Оба способа достаточно не удобны и могут вызывать некоторое раздражение при активной правке шаблонов. Поэтому, для упрощения этой задачи я использую следующий приём (который можно использовать и для других подобных задач):

в конфигурационный файл Emacs (~/.emacs, ~/.emacs.d/init.el или т.п.) я добавляю такой код:
(global-set-key "\C-c\C-p" 
(lambda ()
(interactive)
(slime-eval-async '(mypackage:compile-all-templates)
(lambda (obj) (message "Compilation templates finished")))))
Теперь при нажатии в Emacs сочетания C-c C-p будет вызвана функция mypackage:compile-all-templates, которая вызывает перекомпиляцию шаблонов (или что либо другое, что нужно именно вам).

Ускоряем компиляции шаблонов cl-closure-template в SBCL

Система шаблонов cl-closure-template транслирует код шаблонов в код на CL и компилирует его в исполняемый, что на SBCL ведёт к генерации машинного кода и может занимать существенное время (поскольку SBCL очень медленный компилятор), например, по моему основному проекту пере-компиляция шаблонов занимает чуть более 12 секунда, что довольно сильно напрягает в процесс разработки. Мне бы хотелось, что бы в режиме разработки можно было бы значительно ускорить время компиляции шаблонов, пусть и за счёт снижения их производительности. Как оказалось, решить эту проблему для SBCL можно очень просто за счёт использования переменной sb-ext:*evaluator-mode*:
(defparameter *developer-mode* nil)

(let ((sb-ext:*evaluator-mode* (if *developer-mode* :interpret :compile)))
(closure-template:compile-template :common-lisp-backend ..))
Здесь, когда переменная *developer-mode* установлена в значение отличное от NIL, шаблоны не компилируются, а интерпретируются. Работают они после это, конечно, не так быстро, но в процессе разработки это и не так важно. В результате, время перекомпиляции шаблонов (в режиме разработки) в моём проекте сократилось до 0.6 секунды, т.е. уменьшилось примерно в 20 раз.

среда, 13 октября 2010 г.

ANSI Common Lisp на русском

Судя по всему, издательство Символ-Плюс начало подготовительные работы к выпуску ANSI Common Lisp на русском языке. Если эти планы осуществляться, то это будет фактически первое за 20 лет издание книги о CL на "великом и могучем". Будем ждать.

понедельник, 11 октября 2010 г.

mount-submodule

В RESTAS произошло важное переименование: макрос restas:define-submodule теперь называется restas:mount-submodule - такое имя более точно отражает суть данного макроса. Сейчас изменения доступны в git-версии. Следующий релиз, скорей всего, будет иметь версию 0.1.

среда, 6 октября 2010 г.

Конфликт имён

Я тут всёми силами готовлюсь к выпуску RESTAS-0.1 и под это дело (да с чужой подачи) заинтересовался запустить RESTAS под LispWorks. Вроде, проблем быть было не должно. Но вот засада - при попытке загрузить систему сразу ошибка. Проблема c моим пакетом garbage-pools - его nickname конфликтует с nickname пакета graphics-ports, который содержится в образе LispWorks.

Уже до этого я несколько раз задумывался о возможном конфликте nickname различных пакетов, но реальной ситуации возникало и я особо не пытался разобраться. И вот, шанс проверить гибкость CL.

В общем, найденное решение кажется мне достаточно тривиальным:
#+:lispworks (rename-package '#:graphics-ports '#:graphics-ports nil)
(asdf:operate 'asdf:load-op :restas)
#+:lispworks (rename-package '#:garbage-pools '#:garbage-pools)
#+:lispworks (rename-package '#:graphics-ports '#:graphics-ports '(:gp))
Т.е. до загрузки RESTAS удаляем nickanmes пакета graphics-ports, загружаем RESTAS, удаляем nicknames у пакета garbage-pools, восстанавливаем nicknames у пакета graphics-ports.

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

вторник, 5 октября 2010 г.

RESTAS-0.0.11

Выпустил новую версию RESTAS - 0.0.11.

Хотел бы немого пояснить принцип, по которому я определяю релизы. У меня есть, скажем так, основное приложение, надо которым я работаю в рабочее время и за которое мне платят деньги. Размещено оно на отдельном сервере под управлением Gentoo, установку основных lisp пакетов на нём я произвожу с помощью archimag-lisp-overlay. Разработку RESTAS я произвожу параллельно с разработкой этого приложения. Как только я задействую какие-то новые функции RESTAS в этом приложении и появляется необходимость обновить его на рабочем сервере, так сразу я делаю новый релиз, обновляю ebuild в оверлее и произвожу обновление нужных пакетов. Вот и вся логика ))

воскресенье, 3 октября 2010 г.

parse-route-url

Добавил в RESTAS, новую функцию:
(defun parse-route-url (url route-symbol &optional submodule-symbol)
...)
Поскольку польза от неё, как мне кажется, не вполне очевидна, то должен немного пояснить для чего я её использую.

Я широко использую REST-стиль, в том числе, для взаимодействия между JavaScript-клиентом и веб-сервером. При этом, все данные представляются как ресурсы и идентифицируются с помощью URL, а в качестве обменного формата используется JSON (можно и XML). Сейчас я столкнулся с тем, что часто необходимо с клиента (JavaScript) отправлять данные на сервер для какой-либо обработки, а в отсылаемых на сервер данных содержатся url-идентификаторы объектов. На сервере во время обработки необходимо разбирать эти url-индикаторы на составляющие их параметры (например, для составления запросов к базе). Делать это вручную (с помощью регулярных выражений) очень непродуктивно и совершенно не масштабируемо, поскольку формат этих url зависит от конфигурации среды и для их генерации используется функция restas:genurl. Таким образом, функция restas:parse-route-url является обратной к семейству функций genurl.

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