понедельник, 28 февраля 2011 г.

Декораторы

В предыдущем сообщении я использовал термин "middleware", который показался мне в итоге не очень удачным. Всё же middleware это больше про WSGI или про тот же Clack. А в контексте RESTAS более точным и адекватным видимо является термин "декоратор". Я изменил это в коде, а также добавил несколько новых возможностей.

Теперь restas:define-module и restas:define-route имеют keyword аргумент :decorators, в котором можно указать список декораторов, используемых для преобразования маршрутов. Вместо :decorators в restas:define-module также можно использовать переменную *decorators*, создаваемую при определении модуля. Т.е. теперь указывать декораторы можно при определении модуля, определении маршрута и подключении субмодуля. При этом, декораторы, указанные на различных уровнях, не замещают друг друга, а применяются последовательно.

В качестве примера декоратора, а также как просто удобную возможность, добавил restas:no-cache-decorator:
(defclass no-cache-route (routes:proxy-route) ())

(defmethod process-route :before ((route no-cache-route) bindings)
(setf (hunchentoot:header-out :expires)
(hunchentoot:rfc-1123-date))
(setf (hunchentoot:header-out :cache-control)
"max-age=0, no-store, no-cache, must-revalidate"))

(defun no-cache-decorator (route)
(make-instance 'no-cache-route :target route))
Кстати, функция no-cache из Hunchentoot, делает какую-то ерунду.

четверг, 24 февраля 2011 г.

RESTAS middleware

Я сегодня рассматривал Clack и должен сказать, что мне это определённо не нравится. Впрочем WSGI у меня тоже никогда больших восторгов не вызывал. Слишком универсальный протокол и слишком слабый API. Но, сама концепция middleware (в том виде, как она есть, например, в Pylons) не так уж и плоха. Просто, на мой вкус, её надо сделать более конкретной. Кое какие мысли у меня на эту тему уже крутились и я даже показывал недавно, как можно тонко влиять на процесс обработки запроса. И вот сегодня идея начала приобретать конкретную форму. В частности, изменился макрос restas:mount-submodule, теперь можно писать примерно так:
(restas:mount-submodule -mysubmodule- (#:mymodule middleware1 middleawre2)
..)
Здесь middleware1 и middleware2 это функции, которые должны принимать маршрут и возвращать другой.

В момент построения дерева маршрутов (а это происходит каждый раз при вызове функции restas:recconect-all-routes) для конкретного субмодуля строиться список обрабатываемых им маршрутов и пропускается через цепочку middleware-вызовов. middleware-функция может как угодно изменить маршрут или вообще вернуть другой. Специально для поддержки этого я добавил в cl-routes новый класс routes:proxy-route (который реализует известный паттерн proxy).

Скажем, с помощью модуля restas-directory-publisher можно публиковать директории со статикой:
(restas:mount-submodule -tmp- (#:restas.directory-publisher)
(restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/)
(restas.directory-publisher:*autoindex* t))
Такой код позволит любому просматривать содержимое директории /tmp на сервере, но кто знает что там может быть. Теперь с помощью middleware можно защитить содержимое этого каталога, требуя от посетителей пройти HTTP-авторизацию:
(defclass http-auth-route (routes:proxy-route) () )

(defmethod routes:route-check-conditions ((route http-auth-route) bindings)
(if (call-next-method)
(multiple-value-bind (user password) (hunchentoot:authorization)
(or (and (string= user "hello")
(string= password "world"))
(hunchentoot:require-authorization)))))

(defun http-auth-middleware (route)
(make-instance 'http-auth-route :target route))

(restas:mount-submodule -tmp- (#:restas.directory-publisher http-auth-middleware)
(restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/)
(restas.directory-publisher:*autoindex* t))
Здесь определяется новый класс http-auth-route, наследующий от routes:proxy-route, и для него специализируется метод routes:route-check-conditions, который вызывается для проверки соответствия маршрута условиям запроса. Если маршрут проходит все проверки, то проверяется прошёл ли пользователь HTTP-авторизацию. Функция http-auth-middleware используется для создания таких маршрутов и указывается в списке middlewares макроса restas:mount-submodule.

Для маршрутов, наследующих от routes:proxy-route, имеет смысл определять специализации методов routes:route-check-conditions и/или restas:process-route.

Чуть более сложный пример использования описанных возможностей можно найти здесь.

воскресенье, 20 февраля 2011 г.

Крякнем, плюнем и надёжно скрепим скотчем (с)

Итак, мне удалось запустить локальную версию сайта lisper.ru на своей машине под управлением Mongrel2 без Hunchentoot. В наличии имеется некоторое количество костылей, жуткого оверхеда и ещё не решённых вопросов, но это работает, включая обработку POST-запросов (как application/x-www-form-urlencoded, так и multipart/form-data), работу с cookie и отдачу статики.

Изменение в коде компонентов lisper.ru были самыми минимальными и заключались в переключении с использования Hunchentoot API на библиотеку cl-wsal.

Common Lisp Web servers abstraction layer

Идея cl-wsal зрела у меня уже давно и нужен был лишь маленький толчок, который и был сделан в предыдущем обсуждении. Я вынес из Hunchentoot значительную часть кода, которая может быть полезной в любых веб-серверах для CL в эту библиотеку и с её помощью довольно легко довёл cl-mongrel2 до необходимого состояния.

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

Весь этот код хорошо протестирован, поскольку просто взят из Hunchentoot. Правда, кое-что я изменил. Использование flexi-streams я заменил на babel. Пришлось вырвать кусок из chunga. А также скопировать и несколько отредактировать rfc2388.lisp из одноимённого пакета.

Вся работа над соответствующими изменениями в RESTAS ведётся в бранче Mongrel2. Следующий релиз RESTAS не будет включать этих изменений, посколькуо будет увязан с новым релизом Hunchentoot. А вот после этого я буду полностью переключаться на использование cl-wsal.

четверг, 17 февраля 2011 г.

RESTAS и Mongrel2

Hunchentoot предоставляет очень удобный интерфейс, но есть некоторые сомнения о возможности использования его под высокой нагрузкой, всё таки схема поток на соединения имеет достаточно понятные пределы для масштабирования. Правда, судя по патчам ITA они таки используют Hunchentoot под достаточно высокой нагрузкой, но хотелось бы всё таки иметь и другое решение. Писать асинхронный веб-сервер на CL, который бы предоставлял уровень сервиса сопоставимый с Hunchentoot, меня сейчас не прельщает. Я достаточно долго хотел просто форкнуть Hunchentoot, переделать его на базе iolib с использованием epoll и т.п., но сейчас уже отказался от этой идеи. Отказался после того, как узнал о существовании Mongrel2, который обещает все плюшки асинхронного веб-сервера и при этом не зависит от языка.

Сейчас моя самая большая цель - научить RESTAS работать с Mongrel2 (сохранив при этом возможность работать и с Hunchentoot). Но есть проблема, что Mongrel2 не предоставляет такого интерфейса, как Hunchentoot, он только умеет обрабатывать HTTP и всё. Соответственно, задача сводится фактически к вырыванию довольно больших кусков кода из Hunchentoot и адаптации их для Mongrel2. Но в данный момент я не имею достаточных временных ресурсов для выполнения этой работы. Отсюда и вопрос. Нет ли желающих помочь в этом (достойном) деле? ;)

вторник, 15 февраля 2011 г.

Управление обработкой запросов в RESTAS

Добавил в RESTAS экспериментальную возможность: переменные restas:*before-dispatch-request-hook* и restas:*after-dispatch-request-hook*. В эти переменные можно складывать (pushnew) функции, которые будут вызываться соответственно до и после обработки запроса. restas:*before-dispatch-request-hook* может быть полезен, например, для безусловного требования авторизации от пользователей - не-авторизованных пользователей можно перенаправлять на страницу входа или запрашивать HTTP-авторизацию с помощью hunchentoot:require-authorization. А restas:*after-dispatch-request-hook* можно использовать, например, для сбора статистики обращения к различным URL, или можно делать косметическую пост-обработку результата, скажем, добавлять заголовки ответа.

Для иллюстрации использования restas:*before-dispatch-request-hook* я написал тривиальный пример модуля для сбора статистики, посмотреть его можно здесь.

restas:*before-dispatch-request-hook* и restas:*after-dispatch-request-hook* не позволяют серьёзным образом влиять на обработку запроса (хотя с их помощью и можно вносить небольшие изменения), кроме того - они применяются для всех запросов, обрабатываемых веб-сервером.

Есть другая, более тонкая возможность влиять на ход обработки запросов. Например, если необходимо ограничить доступ к маршрутам из какого-либо модуля или проводить тонкую настройку обработки маршрутов из конкретного модуля.
(in-package #:mymodule)

(defclass myroute (restas:route) ())

(defmethod restas:module-routes ((module (eql #.*package*)) submodule)
(let ((routes (call-next-method)))
(iter (for route in routes)
(change-class route 'myroute))
routes))
Здесь объявляется свой класс маршрутов myroute и для конкретного модуля специализируется метод restas:module-routes, в котором основная работа делается с помощью call-next-method, а затем у полученных маршрутов изменяется класс с restas:route на myroute. Теперь можно определить собственные специализации для методов обработки маршрутов и определить произвольную логику обработки любым удобным способом. Для класса myroute имеет смысл специализировать следующие методы:
  • routes:route-check-conditions (route bindings) - проверяется соответствует ли запрос требования маршрута
  • restas:process-route (route bindings) - здесь происходит вызов обработчика маршрута и, соответственно, генерация контента

среда, 9 февраля 2011 г.

swank-js - удивительное рядом

Поскольку мне приходится писать и отлаживать много JavaScript кода, то я уже давно мечтал о возможность изменять исходный код при работе в Emacs и сразу же отправлять изменения в браузер. Так же очень здорово было бы иметь JavaScript-консоль в Emacs, которая бы реально взаимодействовал с открытой веб-страницей. Или очень часто нужно немного подправить CSS и заставить браузер применить эти изменения без перезагрузки страницы. Звучит несколько фантастически, но сейчас это совершенно реально благодаря проекту swank-js.

Мне, правда, пришлось внести небольшое изменение в оригинальный код, что бы это заработало для меня. Суть отличий в том, что в веб-страницу надо дополнительно включать такой JavaScript-код:
 SwankJS.setup("localhost", {port: 8009});
Мои изменения здесь.

вторник, 8 февраля 2011 г.

RESTAS и JavaScript

Популярность JavaScript в качестве серверного языка стремительно увеличивается, чему способствует в том числе и свойства самого языка - JavaScript это удивительно гибкий и пластичный язык, он даже мягче, чем лисп. Кроме того, в современных веб-приложениях часто логика перемешивается между клиентом и сервером и очень удобно использовать и там и там один и тот же код. cl-closure-template решает многие проблемы разделения логики, но не все. Поэтому, у меня зародилась мысль дать возможность создавать модули для RESTAS на JavaScript. Тем более, что существует CL-JavaScript - достаточно качественная реализация JavaScript на Common Lisp.

restas-javascript - проект, который должен дать возможность смешивать код на Common Lisp и JavaScript при разработке web-приложений на базе RESTAS. Кое-что уже работает. В частности, я уже смог переписать на JavaScript примеры из статьи Hello World - смотрите код в файле demo.js.

Структура этого кода полность аналогична соответствующему кода на Common Lisp, только использует идиомы JavaScript. Доступ к объектам request и reply (которые в CL оформлены в виде специальных переменных) в обработчиках маршрутом осуществляется через this.request и this.reply. Интерфейсы request и reply повторяет функции из документации к Hunchentoot, но именуются в стиле CamelCase, плюс некоторые методы оформлены в виде "активных свойств".

Загрузить данный файл можно следующим образом:
(restas.javascript:execute #P"/path/to/demo.js")
после чего уже можно будет идти в браузер смотреть результат.

С помощью
(restas.javascript:repl)
можно запустить примитивную JavaScript-консоль и поиграться с маршрутами. Что бы несколько упростить это я добавил простейшую реализацию console.log.

Кстати, дизайн интерфейса для JavaScript мне нравится в некоторых аспектах больше, чем для CL.

среда, 2 февраля 2011 г.

Ещё раз про cl-uglify-js

Я уже писал про cl-uglify-js, но тут случилось примечательное событие, которое на мой взгляд стоит отметить отдельно. cl-uglify-js и UglifyJS суть библиотеки-близнецы, от одного автора, развивающиеся синхронно и делающие совершенно одно и то же, просто написанные на разных языках - одна на Common Lisp, а другая на JavaScript (для Node.js). Так вот, теперь система сборки jquery использует именно UglifyJS (вместо Google Closure Compiler!), подтверждение чему можно найти здесь или здесь (раздел про BUILD SYSTEM).

В общем, сейчас можно смело утверждать, что cl-uglify-js (наряду с UglifyJS) претендует на роль одного из лучших (или даже лучшего) решений в своей области.

IE 9 - я удивлён

Я, конечно, слышал, что IE 9 это чудо техники и поддерживает все современные стандарты почти полностью и т.п. Слышал, но не верил. Но вот стало любопытно, попросил админа установить на его машине сей продукт и решил попробовать запустить на нём свой мерчендайзинг. Тут должен заметить, что я никогда не планировал использовать данное приложение под IE. Мои пользователи используют Firefox, разработку я веду в основном с помощью Chromium, время от времени тестирую под Opera. Приложение основано на XHTML + SVG, использует достаточно нетривиальную обработку XML (DOMParser и т.п.). Под этими тремя (Firefox, Chrome, Opera) браузерами приложение заработало не сразу, был целый ряд различий в поведении, так что пришлось искать код, который одинаково работает во всех этих браузерах.

И вот, просто ради любопытства запускаю под IE 9 - и, о чудо, оно просто работает. Вообще без какой-либо адаптации. У меня, честно говоря, культурный шок.