Fork me on GitHub

Подстветка кода с помощью colorize

Для подсветки кода на Common Lisp я люблю использовать colorize, потому что она добавляет в разметку ссылки на Hyperspec. А вот структура разметки получается та ещё, я хочу другую, более современную. Но исходники colorize ужасны и копаться в них я не хочу. Поэтому я написал функцию, которую берёт оригинальную разметку, генерируемую colorize и превращает её в нечто более интересное (для меня):

  1. (defparameter *span-classes*
  2.   '("symbol" "special" "keyword" "comment" "string" "character"))
  3. (defun update-code-markup (markup)
  4.   (labels
  5.       ((bad-span-p (node)
  6.          (and (string-equal (xtree:local-name node) "span")
  7.               (not (member (xtree:attribute-value node "class") *span-classes*
  8.                            :test #'string-equal))))
  9.       ;;---------------------------------------
  10.       (comment-p (node)
  11.          (and (string-equal (xtree:local-name node) "span")
  12.               (string-equal (xtree:attribute-value node "class") "comment")))
  13.       ;;---------------------------------------
  14.       (br-p (node)
  15.          (string-equal (xtree:local-name node) "br"))
  16.       ;;---------------------------------------
  17.       (flatten-spans (node)
  18.          (iter (for el in (xtree:all-childs node))
  19.                (flatten-spans el))
  20.         ;;---------------------------------------
  21.         (when (comment-p node)
  22.            (setf (xtree:text-content node)
  23.                  (xtree:text-content node))
  24.            (xtree:insert-child-after (xtree:make-element "br") node))
  25.         ;;---------------------------------------
  26.         (when (bad-span-p node)
  27.            (iter (for el in (xtree:all-childs node))
  28.                  (xtree:insert-child-before (xtree:detach el) node))
  29.            (xtree:remove-child node))))
  30.    ;;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  31.    (html:with-parse-html (doc (format nil "<div>~A</div>" markup))
  32.       (let ((div (xtree:first-child (xtree:first-child (xtree:root doc)))))
  33.         (flatten-spans div)
  34.         (xtree:with-object (fragment (xtree:make-document-fragment doc))
  35.          ;;---------------------------------------
  36.          (let* ((pre (xtree:make-child-element fragment "div"))
  37.                  (ol (xtree:make-child-element pre "ol")))
  38.             (setf (xtree:attribute-value pre "class")
  39.                   "prettyprint linenums")
  40.             (setf (xtree:attribute-value ol "class")
  41.                   "linenums")
  42.            ;;---------------------------------------
  43.            (iter (for line in (split-sequence:split-sequence-if #'br-p (xtree:all-childs div)))
  44.                   (for i from 0)
  45.                   (let ((li (xtree:make-child-element ol "li")))
  46.                     (setf (xtree:attribute-value li "class")
  47.                           (format nil "L~s" i))
  48.                    ;;---------------------------------------
  49.                    (iter (for el in line)
  50.                           (xtree:append-child li (xtree:detach el))))))
  51.          ;;---------------------------------------
  52.          (html:serialize-html fragment :to-string))))))

Здесь можно видеть не только код, но и непосредственный результат.

Кстати, функция довольно большая. Читать "сплошное месиво кода" на CL бывает не очень приятно. В других языках код обычно "разряжают" с помощью пустых строк. Но в CL это эстетически смотрится как-то не очень. Так что я решил попробовать использовать линии в качестве разделителей. Пока мне кажется, что получается неплохо.

Изменения в mongo-cl-driver

После длительного перерыва снова взялся за mongo-cl-driver и основательно его переделал.

Адаптеры

Для начала выкинул iolib. При чём, не могу сказать о ней ничего плохого, но... Во-первых, до сих пор я использовал mongo-cl-driver только в блокирующем режиме, мои надежды на то, что iolib откроет двери в мир асинхронного программирования не оправдываются - просто никто ничего такого не пишет, никакого веб-сервера и т.п. на iolib до сих пор не появилось. А между тем, Andrew Lyon активно пилит платформу на базе libevent: cl-libevent2, cl-async, drakma-async и даже асинхронный веб-сервер wookie (это при том, что кое-какой веб-сервер есть в самой libevent). Кстати, libevent отлично работает на винде.

В итоге, под впечатлением от pika добавил в mongo-cl-driver поддержку адаптеров и сейчас есть два работающих адаптера: на базе usocket и cl-async. Вернуть адаптер для iolib несложно, но пока не понятно что с ним вообще делать то.

Futures

Долго не мог решить, как же совместить в одной библиотеке синхронный и асинхронный интерфейс. Идти по пути node.js, где каждая функция представлена в двух вариантах (например, fs.readFile и fs.readFileSync) мне не хотелось. Как сделано в pika (каждый адаптер предоставляет свой набор классов) мне тоже не нравится. В итоге остановился на использовании futures : никакого разделения API на синхронный и асинхронный нет, но при использовании асинхронного адаптера возвращается не результат, а future. Сейчас мне кажется, что это очень удачный вариант, за одним но... Я пока не придумал, как нормально организовать обработку ошибок в таком варианте. cl-async-future (по моей просьбе автор выделил её в отдельную библиотеку) предоставляет API и набор макросов. Эти макросы работают просто замечательно если ошибок нет. А вот когда есть ошибки, случается что-то мутное. Пока план в том, что бы взять API и реализовать на его основе аналог Step, но этого я ещё не сделал. Собственно, поэтому пользоваться cl-async адаптером пока не стоит, но можно с ним экспериментировать.

MongoClient

Другим направлением работы было приведение драйвера в соответствии с официальными рекомендациями. В первую очередь, добавлен класс mongo:mongo-client, при создании которого указывается параметры сервера (класс mongo:server-config) и уровень Write Concern (класс mongo:write-concern).

Адаптеры наследуют от mongo:mongo-client и поэтому для создания клиента следует использовать функцию mongo:create-mongo-client:

  1. (mongo:with-client (client (mongo:create-mongo-client :usocket
  2.                                                       :server config
  3.                                                       :write-concern w))
  4.   (print (mongo:$count (mongo:collection (make-instance 'mongo:database
  5.                                                         :client client
  6.                                                         :name "blog"))
  7.                                          "posts"))))

Для упрощения задания Write Concern предопределены следующие константы:

  • mongo:+write-concern-normal+

  • mongo:+write-concern-fsync+

  • mongo:+write-concern-replicas+

  • mongo:+write-concern-journal+

  • mongo:+write-concern-fast+

  • mongo:+write-concern-crazy+

Другие изменения

У функций

  • mongo:update-op

  • mongo:insert-op

  • mongo:delete-op

опциональный параметры превращены в keys для того, что бы добавить key-параметр :write-concern.

Функция mongo:collection-count переименована в mongo:$count. Плюс добавленная функция mongo:$distinct, очень удобная, например, все тэги в этом блоге с её помощью я получаю так:

  1. (mongo:$distinct posts "tags")

Добавил функцию mongo:eval-js:

  1. (mongo:with-client (client (mongo:create-mongo-client :usocket))
  2.   (mongo:eval-js (make-instance 'mongo:database :mongo-client client :name "test")
  3.                  "function (x, y) { return x * y; }"
  4.                  :args #(2 3)))
  5. 6.0d0

Ну и ещё наверное есть пара небольших изменений, о которых сейчас не помню.

Ищу удалённую работу

Могу предложить:

  • Common Lisp.

  • Python. Как веб-разработка (c Django знакомился, но предпочитаю Flask), так и разная логика. Имею опыт использования PyPy в продакшен. На github можно найти пример моего код на Python: python-closure-tempaltes.

  • JavaScript (предпочитаю JQuery, но также приходилось работать с ExtJS). В качестве демонстрации навыков могу предложить такой вот скринкаст: http://www.youtube.com/watch?v=f6b0sQpDGVM .

  • C++, правда, малость подзабыл - уже 4 года практически не писал, но до этого было 7 лет плотной практики с глубоким погружением в разные boost и т.п. В интернете завалялась одна моя небольшая старая библиотека с примером кода на C++: popen++. Кроме того, могу писать и на C, хотя и не очень люблю.

  • XSLT. В своё время написал на нём очень много кода.

  • Имею опыт работы с Oracle/PostgreSQL/MSSQL/Firebird, но сам специалистом по SQL не являюсь, обычно код для БД писали специально обученные люди.

  • Работал с MongoDB и RabbitMQ.

Более 10 лет занимаюсь разработкой программного. Последние полтора года работаю удалённо (правда, регулярно посещаю офис) в Магнит (Тандер) в должности главного специалиста (в основном, выполняю роль архитектора).

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

Контакты:

E-mail:archimag at gmail.com
Skype:archimag-dev

Мой боекомплект веб-разработчика на Common Lisp

  • Hunchentoot - всё ещё лучший веб-сервер на Common Lisp. Работает по схеме "поток на соединение". Надеюсь в будущем у нас будут более современные веб-сервера. В принципе, у меня есть кое-какие наработки для работы под управлением Mongrel2, но на практике я пока использую исключительно Hunchentoot.

  • RESTAS - мой веб-фреймвок, делающий основной акцент на контролёре и повторном использовании кода. Из фреймворков на других языках наиболее близок к Flask (просто удивительно близок если учесть, что основную работу над RESTAS я выполнил в то время, когда ещё не знал о существовании Flask).

  • cl-closure-template - моя библиотека шаблонов, реализация спецификации Google Closure Templates.

  • cl-who - иногда использую её для небольших тестовых примеров, никогда для настоящих приложений.

  • cl-sanitize - моя библиотека для очистки HTML от нежелательного содержимого (совершенно необходима, если пользователям дозволяется вводить html-данные). Вдохновлена Sanitize, откуда были взяты whitelists и тестовые данные.

  • cl-data-forms - моя библиотека для обработки и валидации форм. Вдохновлена WTForms, но сильно от неё отличается.

  • postmodern - заслуженная и уважаемая библиотека для доступа к PostgreSQL, можно даже сказать, что эталонная библиотека для работы с СУБД в мире CL.

  • mongo-cl-driver - моя библиотека для работы c MongoDB. Не сказать, что бы она сильно функциональна, но мне пока хватает. Гораздо раньше появилась cl-mongo, но я считаю её совершенно уродливой как снаружи, так и внутри. Кроме того, mongo-cl-driver умеет работать асинхронно, хотя мне это ещё и не пригодилось (и скорей всего из-за этого дизайна далёк от совершенства).

  • colorize - библиотека для подсветки исходного кода, поддерживает не очень много языков, зато имеет совершенно уникальную поддержку для Common Lisp: превращает все стандартные функции, классы, переменные в ссылки на HyperSpec.

  • cl-docutils - библиотека для обработки разметки в формате reStructuredText. На мой взгляд сильно раздута и малость запутана, но это лучшее, что есть в этом классе для Common Lisp. Другой неплохой библиотекой для обработки языка разметки является cl-markdown, но я не люблю Markdown.

  • local-time - мощная и удобная библиотека для работы со временем.

  • ironclad - мощная и удобная криптографическая библиотека.

  • cl-pdf и cl-typesetting - генерация PDF. Без документации, но с несколькими примерами. Довольно удобные библиотеки, не лишенные своих глюков и особенностей. Я имею форк cl-pdf, который содержит несколько патчей, проигнорированных в основном списке рассылки, самый главный - непосредственная поддержка TTF-шрифтов.

  • salza2 и zip - работа с архивами.

  • cl-sphinx - моя библиотека для создания документации, реализующая некоторые возможности Sphinx.

  • cl-libxml2 - моя первая библиотека. Собственно, с её написания и началось моё глубокое знакомство с CL. Поскольку это был первый опыт, то дизайн, тот ещё. Ну и ресурсами надо управлять вручную. Зато может почти всё, что предлагает libxml2. Даже можно писать свои расширения для XSLT-процессора.

Ещё одна библиотека - cl-data-forms

При разработке веб-приложений есть такая неприятная вещь, как обработка форм. Вроде бы довольно тривиально, но без системного подхода превращается в какой-то мрак. Я давно облизывался на WTForms и вот теперь имею подходящее решение - cl-data-forms.

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

Прежде, чем рассказывать про использование cl-data-forms, необходимо сказать несколько слов про data-sift. В принципе, я уже несколько раз упоминал эту библиотеку (и даже успел её использовать в RESTAS), но очень мало, плюс я несколько её переработал.

Там, где есть взаимодействие компьютера с человеком на основе текстовых форматов, возникает проблема преобразование внутренних представлений в текстовый, понятный для человека вид, и обратно. Мне очень понравилась библиотека cl-data-format-validation, призванная упростить эту проблему. Но с этой библиотекой есть одна проблема - она распространяется под лицензией GPL v3. Собственно, эта и подтолкнуло меня меня к созданию альтернативного решения под более мягкой лицензией.

data-sift определяет две обобщённые функции:

  • compile-parse-rule (rule &key &allow-other-keys)

  • compile-render-rule (rule &key &allow-other-keys)

compile-parse-rule принимает на вход правило, описывающее тип значения, и создаёт на его основе замыкание, которое может использоваться для валидации и, возможно, трансформации текстового значения, compile-render-rule делает совершенно обратное, т.е. возвращает функцию, которая может превратить значение в текст. Использовать это можно, например, так:

  1. (funcall (data-sift:compile-parse-rule 'integer :min-value 0 :max-value 100) "56")

data-sift уже включает в себя поддержку нескольких форматов данных (правда, всё это пока довольно сыро), а в своём приложении можно определить дополнительные форматы, специфичные для данного приложения. Например, так можно определить формат date, соответствующий элементу <input type="date" /> из HTML5:

  1. (defmethod data-sift:compile-parse-rule ((rule (eql 'date)) &key)
  2.   (alexandria:named-lambda date-parser (str)
  3.     (handler-case
  4.         (local-time:parse-rfc3339-timestring str :allow-missing-time-part t)
  5.       (error ()
  6.         (data-sift::vfail "Invalid date")))))
  7. (defmethod data-sift:compile-render-rule ((rule (eql 'date)) &key)
  8.   (alexandria:named-lambda date-renderer (date)
  9.     (local-time:format-timestring nil date :format local-time:+rfc3339-format/date-only+)))

Теперь можно вернуться к cl-data-forms. Сразу буду показывать код:

  1. (define-form-class user-info-form ()
  2.   ((username
  3.     :initarg |username|
  4.     :initform nil
  5.     :required "Username is required"
  6.     :label "Username")
  7.    (email
  8.     :initarg |email|
  9.     :initform nil
  10.     :stype data-sift:email
  11.     :required t
  12.     :label "Email Address")))
  13. (define-form-class user-password-form ()
  14.   ((password
  15.     :initarg |password|
  16.     :initform nil
  17.     :required "Password is required"
  18.     :stype (string :min-length 6 :message "Password must be at least 6 characters.")
  19.     :label "Password"
  20.     :itype "password")
  21.    (confirm-password
  22.     :initarg |confirmPassword|
  23.     :initform nil
  24.     :itype "password"
  25.     :label "Confirm password")))
  26. (define-form-class registration-form (user-info-form user-password-form)
  27.   ((birthday
  28.     :initform nil
  29.     :initarg |birthday|
  30.     :stype date
  31.     :label "Birthday"
  32.     :itype "date")
  33.    (keep-me-signed
  34.     :initform nil
  35.     :initarg |keepMeSigned|
  36.     :label "Keep me signed-in on this computer."
  37.     :itype "checkbox")))

Здесь определяется три формы:

  • user-info-form - предназначена для ввода имени пользователя и email

  • user-password-form - предназначенная для ввода пароля и его подтверждения

  • registration-form - включает в себя user-info-form и user-password-form, а также два дополнительных поля - день рождения и флаг "запомнить меня на этом компьютере"

Каждый макрос define-form-class создаёт новый класс. При описании слотов:

  • Можно использовать все те же самые параметры, что и при обычном defclass.

  • Параметр :initarg в описании слота является обязательным - он используется в последующем для получения данных формы. В коде выше вместо стандартных keyword-ов я использовал экранированные символы - так получается более красивый HTML (об этом ниже).

  • Параметр :requried используется для указания того, что поле является обязательным. Если указана строка, то она будет использоваться для создания сообщения об ошибке.

  • В параметре :stype можно указать формат для библиотеки data-sift и на его основе будет происходить проверка и преобразование данных.

  • Дополнительно можно указать любые другие параметры (в коде выше это :label и :itype) - они никак не обрабатываются, а просто сохраняются и могут быть использованы произвольным образом в зависимости от потребностей приложения.

Вот скриншот, полученный на основе registration-form (поскольку использован HTML5, то не во всех браузера поле для ввода даты будет именно таким, я использовал Chromium):

Скриншот HTML формы

Объекты форм можно создавать:

  • С помощью стандартного make-instance

  • С помощью функции make-form, которая принимает имя класса формы и набор параметров в формате alist (как post-параметры в Hunchentoot). При этом, сопоставление параметров слотам производится на основе параметра :initarg, указанного при описании слота.

Проверка значения слота производится при каждом его изменении с помощью setf slot-value в том случае, если для задания нового значения используется строка. В случае ошибки валидации исключение не возбуждается (подавляется), а сообщение сохраняется во внутренней структуре формы, его можно получить с помощью field-error.

Важный момент, cl-data-forms не имеет никаких функций для генерации HTML. Вместо этого, она позволяет добавить в описание слота любые произвольные данные, которые могут быть получены вместе со значением и сообщением об ошибке при вызове функций: form-data-alist и form-data-plist (разница между ними только в формате). Например:

  1. EXAMPLE> (data-forms:form-data-alist (make-instance 'user-info-form '|username| "Andrey" '|email| "fake"))
  2. ((|username| (:VALUE . "Andrey")
  3.              (:LABEL . "Username"))
  4.  (|email| (:ERROR . "Doesn't look like a valid email.")
  5.           (:VALUE . "fake")
  6.           (:LABEL . "Email Address")))

На основе такого описания приложение может создавать HTML в своём собственном стиле. Для теста я использовал специальный шаблон для cl-closure-template.

Быстро узнать корректно ли заполнена форма можно с помощью функцию is-valid.

Полный код примера здесь , а то я и так уже слишком много написал. Для работы этого примера необходимы самые последние версии data-sift, restas, cl-closure-template и cl-data-forms.

P.S. Если посмотреть на описание формы, то видно, что оно полностью декларативное (за исключением пары нюансов). А значит, можно пробовать на основе такого описания генерировать JavaScript код для того, что проводить модные проверки валидности данных ещё на клиенте.

Документация для cl-closure-template

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

Наполовину это перевод документации на Google Closure Templates, местами несколько сокращённый, дабы не сказать лишнего. Остальная часть специфична именно для cl-closure-template. Особо рекомендую раздел Использование с Common Lisp .

Документация ещё будет доводиться до ума, но это скорей стилистическая работа, фактический материал будет меняться мало (надеюсь). Когда русский вариант будет окончен, будет перевод на английский.

Вообще, писать документацию совершенно адская работа (ибо не доставляет) и, что-бы как-нибудь разнообразить это дело, я попутно реализовал такую классную вещь, как Injected Data. Реализация оказалась довольно тривиальной, а вот профита от этой возможности действительно много.

ext-blog и тема iSimple

В Quicklisp есть пакет ext-blog, основанный на RESTAS. После последних изменений в RESTAS он, конечно, перестал работать и я решил его починить и сделать Pull Request автору. Ну заодно и посмотрел в работе этот движок.

Движок довольно забавный и в нём есть две темы, портированные с WordPress. Одну из этих тем (iSimple) я решил добавить к arblog и временно изменил оформление данного блога на эту тему. Пусть побудет так до следующего поста.

Последние изменения в cl-closure-template

Тут незадолго до нового года вышел новый релиз Google Closure Templates и я решил посмотреть, что же изменилось в библиотеке пока я занимался разными другими делами. Оказалось, что разрыв в возможностях с cl-closure-template уже довольно внушительный, так что я решил его немного сократить. Правда, некоторые решения оригинальной реализации мне не понравились и я решил от них отклониться. Итак, в последнее время в cl-closure-template были добавлены:

  • Поддержка литералов для списков: [<expr1>, <expr2>, ...]. Например: [1, 'two', [3, false]]. [] - пустой список.

  • Поддержка литералов для словарей: {<keyExpr1>: <valueExpr1>, <keyExpr2>: <valueExpr2>, ...}. Например: {'aaa': 42, 'bbb': 'hello'}. {:} - пустой словарь. В оригинальной версии вместо фигурных скобок используются квадратные и мотивируется это тем, что фигурные уже используются для команд. И в начале я тоже сделал на квадратных. Но потом решил что это не нормально, что гораздо естественнее и приятнее использовать всё таки фигурные скобки. Скорей всего такое решение в оригинальной реализации обусловлено особенностями парсера, но у меня таких особенностей нет, поэтому в итоге переделал на фигурные скобки.

  • Добавлена функция keys. keys(map) - возвращает ключи словаря map в виде списка.

  • Добавлена функция augmentMap. augmentMap(baseMap, additionalMap) - создаёт новый словарь (ну, реализация несколько сложнее, не просто словарь), содержащий записи как из additionalMap, так и из baseMap. При поиске ключа в таком словаре он сначала ищется в additionalMap, а если не найден, то поиск продолжается в baseMap. Для Common Lisp получилась хорошая реализация, без лишних движений, а вот в случае JavaScript приходиться заниматься копированием additionalMap.

  • Добавлена функция strContains. strContains(str, subStr) - проверят является ли subStr частью str.

  • Добавлена функция isNonnull. isNonnull(value) - возвращает true если value определенна и не равна null.

  • Реализована возможность вызывать (call) шаблоны из других namespace. Например:

    {namespace mytemplate.test1}
    
    {template helloWorld}
        Hello world!
    {/template}

    {namespace mytemplate.test2}
    
    {template callHelloWorld}
        {call mytemplate.test1.helloWorld /}
    {/template}

    В случае Common Lisp вызвать таким образом можно только зарегистрированный шаблон, а в JavaScript всё что угодно.

  • Добавлена команда let. Мне не понравилось как она сделана в Google Closure Templates - не ясно как определяется область видимости создаваемых переменных. При чём, не ясно как в спецификации, там и при просмотре кода шаблоны так же могут быть трудности. Поэтому я решил сделать вариант с более чёткой структурой: {let $<identifier1>="<expression1>" $<identifierN>="<expressionN>"}...{/let}

    Пример:

    {let $x="1" $y="$foo.bar + 10"}
      // Здесь код, использующий $x и $y
    {/let}

    Честно говоря, ранее у меня уже была команда with, которая делала то же самое. Но let, конечно, куда лучше подходит по названию, плюс я немного поправил синтаксис. Правда, при этом была потеряна одна возможность (rendering to string) оригинальной реализации, но я считаю её совершенно несущественной.

В Google Closure Templates сейчас есть такая штука, как "delegates", с которой связанно несколько команд: delpackage, deltemplate и delcall. Я считаю данный функционал исключительно мутным и реализовывать что-либо подобное не хочу. А вместо это предлагаю, как мне представляется, куда более простой и понятный механизм - прототипы пространств имён.

Рассмотрим Common Lisp backend. При компиляции namespace создаётся package с таким же именем, а в нём функции для каждого шаблона из namespace. Но эти функции на самом деле никакой реализации не содержат, а просто обращаются к объекту *ttable*, находящемся в том же пакете. *ttable* это объект класса closure-template:ttable, который содержит в себе словарь с реальными обработчикам. Так вот, при создании этого объекта ему можно указать prototype - другой объект класса ttable. Вот небольшой пример, скажем у нас есть такой незамысловатый namespace:

{namespace myapp.view.base}

{template helloWorld}
    Hello world!
{/template}

{template mainPage}
    <h1>{call helloWorld /}</h1>
{/template}

Если теперь выполнить:

(closure-template:ensure-ttable-package
  '#:myapp.view.mytheme
  :prototype (closure-template:package-ttable '#:myapp.view.base)
)

И скомпилировать файл с шаблонами:

{namespace myapp.view.mytheme}

{template helloWorld}
    Привет мир!
{/template}

То станет возможным выполнить следующий код:

CL-USER> (myapp.view.base:main-page)
"<h1>Hello world!</h1>"
CL-USER> (myapp.view.mytheme:main-page)
"<h1>Привет мир!</h1>"

Надеюсь демонстрация получилась достаточно наглядной. Для JavaScript никакой специальной поддержки нет, достаточно просто реализовать такое поведение написав несколько строк на JavaScript:

myapp.view.mytheme = (function () {
    function C () {}
    C.prototype = myapp.view.base;
    return new C;
})();

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

Обновил restas-directory-publisher

Добавил в макрос restas:define-module поддержку декларации :render-method, которая позволяет задать способ по-умолчанию для генерации итогового ответа. На маршруты, у которых задан :render-method в restas:define-route, указание :render-method при определении модуля никакого влияния не оказывает.

Переработал restas-directory-publisher: привёл код в соответствие с последними изменениями RESTAS и cl-closure-template, удалил зависимость от iolib (ибо нефиг), а также удалил встроенную поддержку CGI - редко нужна, тянет за собой hunchentoot-cgi и может быть легко реализована в пользовательском коде.

Собственно, я как раз хотел показать возможность использования декларации :render-method в restas:define-module и restas:mount-module именно на примере поддержки CGI.

Сейчас файл restas-directory-publisher.asd выглядит так:

(defsystem #:restas-directory-publisher
  :defsystem-depends-on (#:closure-template)
  :depends-on (#:restas #:local-time)
  :pathname "src"
  :serial t
  :components ((:closure-template "autoindex")
               (:file "directory-publisher")
)
)

Т.е. первым делом компилируется файл с шаблонами src/autoindex.tmpl, после чего модуль #:restas.directory-publisher можно уже определить следующим образом:

(restas:define-module #:restas.directory-publisher
  (:use #:cl #:iter)
  (:export #:*directory*
           #:*directory-index-files*
           #:*autoindex*
           #:*ignore-pathname-p*
           #:pathname-info
           #:hidden-pathname-p
)

  (:render-method #'restas.directory-publisher.view:autoindex)
)

restas.directory-publisher.view:autoindex - это просто функция, создаваемая при компиляции файла шаблонов.

Модуль #:restas.directory-publisher содержит один единственный маршрут, который в основном возвращает файлы для обработки которых restas.directory-publisher.view:autoindex вызываться не будет (а будет вызываться только при генерации страницы autoindex если установлена опция restas.directory-publisher:*autoindex*).

И вот теперь я могу захотеть публиковать через restas-directory-publisher не только файлы, но и дать возможность исполнять CGI-скрипты (необходимый функционал есть в hunchentoot-cgi), например, для публикации веб-интерфейса к git с помощью gitweb.cgi. Вот полный код:

(asdf:operate 'asdf:load-op '#:restas-directory-publisher)
(asdf:operate 'asdf:load-op '#:hunchentoot-cgi)

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


(in-package #:homesite)

(defclass cgi-handler () ())

(defmethod restas:render-object ((renderer cgi-handler) (file pathname))
  (cond
    ((and (string= (pathname-type file) "cgi"))
     (hunchentoot-cgi::handle-cgi-script file)
)

    (t
     (call-next-method)
)
)
)


(restas:mount-module -gitweb- (#:restas.directory-publisher)
  (:url "/gitweb/")
  (:render-method (make-instance 'cgi-handler))
  (restas.directory-publisher:*directory* #P"/usr/share/gitweb/")
  (restas.directory-publisher:*directory-index-files* '("gitweb.cgi"))
)


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

Теперь по адресу http://localhost:8080/gitweb/ можно будет наблюдать стандартный gitweb-интерфейс к своим проектам (ну, необходимую настройку gitweb я опускаю).

Правда, у пакета hunchentoot-cgi есть какие-то проблемы: gitweb.cgi завёлся без проблем, а вот php-cgi работать не хочет, плюс немного гибкости не хватает. Надо будет как-нибудь что-нибудь там пропатчить.

Чуть более развёрнутый пример использования restas-directory-publisher можно посмотреть здесь: https://github.com/archimag/restas-directory-publisher/blob/master/example/homesite.lisp

Лёгким движением руки брюки превращаются…

Уже давно при разработке RESTAS меня преследовала одна идея, которую я хотел бы иметь возможность реализовать и которую я даже рассматривал в качестве этакой лакмусовой бумажки состояния проекта. До сих пор получалось не очень, но вот теперь у меня кажется есть всё необходимое.

Итак, вот допустим я много и усердно трудился над arblog и получился такой хороший движок для блога, что возникло много желающих им пользоваться, но далеко не все имеют выделенный сервер, на котором можно было бы развернуть необходимое lisp-окружение. И тогда меня может вдруг осенить - а почему бы не сделать движок многопользовательским и не составить конкуренцию Blogger? (ну мало ли какие глупые мысли порой приходят в голову).

С одной стороны я не хочу превращать arblog в "multi-authors blog engine", а с другой и дублировать функционал в новом движке тоже не хочу, а хочу воспользоваться ранее написанным и отлаженным функционалом. Собственно, ниже демонстрация того, как лёгким движением мысли можно на базе одно-пользовательского arblog создать небольшую коллективную платформу для ведения блогов.

Итогового результата я добиваюсь следующим кодом:

(restas:define-module #:my-multi-authors-blog
  (:use #:cl)
)


(in-package #:my-multi-authors-blog)

(restas:mount-module -public- (#:arblog.public)
  (:decorators '@multi-authors)
)


(restas:mount-module -admin- (#:arblog.admin)
  (:url "/admin/")
  (:decorators '@multi-authors 'arblog:@admin)
)


(restas:mount-module -static- (#:arblog.static)
  (:url "/static/")
)

От кода из моего предыдущего сообщения данный отличается тем, что не производится никакой настройки окружения, а при монтировании модулей #:arblog.public и #:arblog.admin используется декоратор @multi-authors:

(defclass multi-authors-arblog-route (routes:proxy-route) ())

(defun @multi-authors (route)
  (make-instance 'multi-authors-arblog-route :target route)
)

Всё интересное в данном случае происходит за счёт переопределение generic-методов для multi-authors-arblog-route.

  1. Изменяем шаблоны маршрутов:

    (defmethod routes:route-template ((route multi-authors-arblog-route))
      (append (routes:parse-template ":author")
              (call-next-method)
    )
    )

    таким образом, в начало каждого шаблона маршрута добавляется переменная :author. Например, шаблон "tags/:tag" превращается в шаблон ":author/tags/:tag". И так для всех маршрутов в модулях #:arblog.public и #:arblog.admin.

  2. Заставляем обработчики маршрутов и проверки дополнительных условий выполняться в контексте конкретного автора:

    (defun author-settings (author)
      (restas:make-context
       `((arblog:*blog-name* . ,author)
         (arblog.internal.datastore:*datastore* . ,(make-instance 'multi-authors-datastore :author author))
         (arblog.internal.markup:*markup* . ,(make-instance 'arblog.markup.rst:arblog-rst-markup))
         (arblog.internal.theme:*theme* . ,(make-instance 'arblog.theme.mirev:arblog-mirev-theme))
    )
    )
    )


    (defmethod restas:process-route :around ((route multi-authors-arblog-route) bindings)
      (let ((author (cdr (assoc :author bindings :test #'string=))))
        (restas:with-context (author-settings author)
          (call-next-method)
    )
    )
    )


    (defmethod routes:route-check-conditions ((route multi-authors-arblog-route) bindings)
      (let ((author (cdr (assoc :author bindings :test #'string=))))
        (restas:with-context (author-settings author)
          (call-next-method)
    )
    )
    )

    Т.е. контекст выполнения не задаётся при монтировании модуля, а формируется динамически на основе информации об обрабатываемом URL (из которого извлекается имя автора блога). Функция #'author-settings в реальном приложении могла бы читать настройки конкретного автора из базы и формировать контекст на основе этих настроек.

  3. Исправляем автоматическую генерацию URL, которая нарушена из-за изменения шаблонов маршрутов в коде выше:

    (defmethod restas:make-route-url ((route multi-authors-arblog-route) bindings)
      (restas:make-route-url (routes:route-template route)
                             (list* :author (blog-author arblog.internal.datastore:*datastore*)
                                    bindings
    )
    )
    )

В данном коде важную роль играет класс multi-authors-datastore, который инициализируется с помощью имени автора блога и реализует интерфейс, определённый в пакете #:arblog.policy.datastore. Поскольку у меня сейчас есть готовая реализация этого интерфейса - arblog.datastore.mongodb:arblog-mongo-datastore, то проще всего было бы создавать для каждого автора отдельную базу. Но возможно некоторым эстетам такой вариант не понравится и поэтому я немного улучшил реализацию arblog-mongo-datastore с тем, что бы можно было сделать так:

(defclass multi-authors-datastore (arblog.datastore.mongodb:arblog-mongo-datastore)
  ((author :initarg :author :reader blog-author))
  (:default-initargs
   :dbspec '(:name "multi-authors-blog")
)
)


(defmethod arblog.datastore.mongodb:make-query ((datastore multi-authors-datastore) &rest args)
  (let ((query (call-next-method)))
    (setf (gethash "author" query)
          (blog-author datastore)
)

    query
)
)

Тут всё просто: при обращении к MongoDB к запросу добавляется поле "author", так что сообщения различных авторов могут спокойно жить в одной коллекции и не мешать друг-другу.

Для теста осталось реально добавить пару пользователей:

(defun add-author (author password)
  (arblog.policy.datastore:datastore-set-admin
   (make-instance 'multi-authors-datastore :author author)
   author
   password
)
)


(add-author "ivanov" "111")
(add-author "petrov" "222")

и запустить приложение:

(restas:start '#:my-multi-authors-blog :port 8080)

Есть один недостаток: ссылка с названием блога, которая показывается на каждой странице, введёт к корню сайта. Что бы это исправить надо немного поправить тему, но рассказ про темы запланирован на другой раз.

Кстати, делать на практике такое с arblog у меня пока никакого желания нет, но зато я планирую использовать такой подход для restas-colorize - я хочу переработать этот модуль, добавить некоторые возможности Gist и использовать как на персональном сайте, так и на http://lisper.ru/apps/format/

P.S. Полный код здесь: https://github.com/archimag/arblog/blob/master/examples/multi-authors-blog.lisp