пятница, 25 марта 2011 г.

cl-sanitize

Написал cl-sanitize - библиотеку для очистки HTML от нежелательного содержания. Точнее, не написал, а портирова Sanitize (Ruby). Хотя "портировал" тоже не совсем верное слово, ибо реализация алгоритма очистки тривиальна и его я написал сам (это просто быстрее, чем разбирать код на Ruby), но вот самое важное - конфигурационные данные и набор тестов взял из Sanitize.

cl-sanitize основана на cl-libxml2. Я сомневался некоторое время, что взять за основу cxml или cl-libxml2 (сомнения тем более оправданны, что cxml и cl-libxml2 плохо совместимы между собой из-за конфликта имён), но всё таки решил использовать cl-libxml2. Во-первых, оригинальная версия основанна на libxml2, так что это позволило легко получить идентичные результаты на тестах. Во-вторых, я не имею такого доверия к "Closure HTML", как к libxml2. А это очень важный момент, ибо после просмотра тестов мне стало немного страшно ходить в интерент.

В процессе пришлось немного допилить cl-libxml2 - как то мне сейчас несколько страшновато заглядывать внутрь, всё таки это был фактически мой первый серьёзный опыт использования CL, отрефракторить бы её основательно, да боюсь, что до этого руки у меня не скоро дойдут.

вторник, 15 марта 2011 г.

Новый парсер для cl-closure-template

Полностью переписал парсер для cl-closure-template на базе esrap. Я вообще давно хотел переписать его на какой-нибудь более серьёзной базе, но те решения для парсинга на CL, которые мне попадались, меня совершенно не устраивали - запутанно и слишком громоздко. esrap понравилась мне сразу, во-первых это PEG, во-вторых описание грамматики можно разбивать на множество отдельных кусков, а не загонять их в один большой макрос, в-третьих - описание грамматики объединяется с кодом по обработке, так что никаких раздельных стадий лексического и синтаксического анализа - всё объединено.

Вообще, esrap принципиально очень сильно похожа на моё старое решение (которое я подсмотрел в коде dokuwiki, а туда оно попало из какого-то решения на Perl), но гораздо совершенней и основано на более выразительных инструментах. Благодаря принципиальной схожести переписывание оказалось довольно простым и заняло 2 дня. При этом, не сказать, что размер код сильно уменьшился, но вот соответствие оригинальной спецификации стало значительно более точным. Например, появилась возможность использовать экспоненциальную форму записи для чисел с плавающей точкой, целые в шестнадцатеричной форме, или напрямую писать коды Unicode в строковых литералах.

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

Мне пришлось исправить несколько своих шаблонов по одной причине - старая версия дозволяла использовать символы { и } достаточно свободным образом (что противоречит спецификации) и пришлось завернуть соответствующие куски в literal секции. Плюс, было несколько мелких опечаток, которые проглатывались старой версией. Ещё в старой версии можно было записывать имена переменных как $foo-bar-x, что противоречит спецификации и я это запретил, но возможность записывать имена шаблонов в "лисп-стиле" пока всё таки оставил (может тоже запретить?).

Полный код парсера здесь.

P.S. Если придётся что-нибудь ещё парсить, то однозначно буду использовать esrap.

четверг, 3 марта 2011 г.

Ещё пара полезных декораторов

В веб-приложениях для отдачи статических файлов принято использовать возможности веб-серверов (Apache, Nginx), поскольку традиционные языки веб-разработки (Python, Ruby, PHP, Perl) справляются с этой задачей очень плохо. Однако, это связано с рядом ограничений. Во-первых, нельзя автоматически генерировать ссылки на основе информации о маршрутах. Но если это ещё можно пережить (хотя, честное слово, очень неудобно), то есть куда более серьёзная проблема - часто нужен "управляемый" доступ к статическим файлам. Например, скрипт должен проверить права доступа. Для решения этой дилеммы была придумана техника, когда скрипт выполняет свою логику, а реальную передачу файла делегирует веб-серверу путем настройки специальных заголовков ответа. Для nginx это X-Accel-Redirect, а для Apache - X-Sendfile.

При разработке на Common Lisp проблема отдачи статических файлов стоит значительнее менее остро, ибо язык сам по себе намного быстрее и Hunchentoot справляется с этой работой достаточно быстро. Я для публикации статических файлов пользуюсь restas-directory-publisher - модулем для RESTAS. Однако, следует признать, что возможны ситуации, когда отдавать статику средствами CL окажется не очень разумно.

Тут вот и возникла идея обеспечить возможность использования механизма XSendfile прозрачным для основной логики образом. В итоге, я добавил в RESTAS соответствующие декораторы для Apache и Nginx.

Ниже я приведу возможные примеры использования этих декораторов в пакете restas-doc, обеспечивающего работу http://restas.lisper.ru/.

для публикации файлов я использую такой код:
(restas:mount-submodule -publisher- (#:restas.directory-publisher))
При этом, сами файлы с документацией лежат в каталоге /usr/share/doc/restas/.

@apache-xsendfile
При работе с Apache всё просто:
(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@apache-xsendfile))
а в конфиге Apache необходимо установить значение параметра XSendFilePath таким образом, что бы публикуемые файлы оказались внутри указанного каталога. Самый простой путь
XSendFilePath /
Но это может оказаться не очень разумным с точки зрения администрирования, так что в данном случае можно и так
XSendFilePath /usr/share/doc/restas/
Более подробная информация о настройке сервера есть в официальной документации.

@nginx-accel-redirect
Настройка nginx является более сложной. Необходимо создать internal-секцию и дальше есть два варианта: либо установить root, либо задать alias. В зависимости от выбранного варианта необходимо различным образом формировать заголовок X-Accel-Redirect. Для поддержки этих вариантов в RESTAS добавлено три переменных:
(defvar *nginx-internal-location* nil)
(defvar *nginx-internal-alias* nil)
(defvar *nginx-internal-root* nil)
и их надо настроить точно в соответствии с настройками nginx. При этом, переменные *nginx-internal-alias* и *nginx-internal-root* являются взаимоисключающими и одна из них должна быть установлена в NIL. В принципе, их можно выставить глобально, но RESTAS позволяет сделать лучше. Например, для такого конфига nginx:

location /restas {
internal;
root /usr/share/doc;
}
Код на CL может быть таким:
(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@nginx-accel-redirect)
(restas:*nginx-internal-location* #P"/restas/")
(restas:*nginx-internal-root* #P"/usr/share/doc/"))
Настройка с root представляется мне довольно неудобной и если nginx поднят только для CL, то гораздо проще делать так
location /protected/ {
internal;
alias /usr/share/doc/restas/;
}
и
(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@nginx-accel-redirect)
(restas:*nginx-internal-location* "/protected/")
(restas:*nginx-internal-alias* "/usr/share/doc/restas/"))
Вообще, описанный функционал обеспечивает очень важную возможность работать со статическими файлами средствами CL, например, создавать повторно используемые компоненты, а в случае возникновения необходимости легко подключить механизм XSendFile с помощью подключения нужного декоратора.

Я пока разбирался с этими вопрос успел посмотреть советы по использованию XSendFile с RoR и Django - там это носит вид хаков, жёстко прибивающих код к конкретной конфигурации веб-сервера. Этот факт подкрепил моё мнение, что модель декораторов RESTAS вкупе с возможностями CL (такими, как CLOS) обеспечивает более гибкие и мощные возможности, чем более традиционная схема с middleware-компонентами.

Прозрачное использование Parenscript

Я использую Parenscript в реализации cl-closure-template. Но обычный код предпочитаю писать на JavaScript. На то есть несколько причин и одна из самых существенных - возникающие инфраструктурные трудности. На самом деле я бы хотел просто размещать файлы с кодом на Parenscript среди других статических файлов и больше ни о чём не думать, а оно пусть там как-нибудь само обрабатывается. После добавления в RESTAS декораторов реализовать такое поведение оказалось очень просто:
(defclass ps-compile-route (routes:proxy-route) ())

(defmethod restas:process-route ((route ps-compile-route) bindings)
(let ((result (call-next-method)))
(cond
((and (pathnamep result)
(string= (pathname-type result) "parenscript"))
(setf (hunchentoot:content-type*)
"text/javascript")
(let ((*package* (find-package '#:ps)))
(ps:ps-compile-file result)))
(t result))))

(defun @ps-compile (origin)
(make-instance 'ps-compile-route :target origin))
В случае если основной обработчик маршрута возвращает pathname с расширением ".parenscript", то данный декоратор компилирует его содержимое в код на JavaScript и отправляет клиенту, иначе возвращает результат без какой-либо обработки. Теперь я просто публикую статические файлы
(restas:mount-submodule -static- (#:restas.directory-publisher @ps-compile)
(restas.directory-publisher:*directory* #P"/path/to/static/")
И если мой Parenscript-код находится в файле "/path/to/static/js/core.parenscript", то в HTML он подключается как
<script src="/js/test.parenscript"></script>

среда, 2 марта 2011 г.

Интернет-магазина на Common Lisp

Если вы мечтали купить что-нибудь в интернет-магазине, написанном на Common Lisp, то с недавних пор имеете такую счастливую возможность (особенно, если живёте в Питере) - www.320-8080.ru, разрабатывается rigidus-ом на основе RESTAS и cl-closure-template!