Лёгким движением руки брюки превращаются…
Уже давно при разработке RESTAS меня преследовала одна идея, которую я хотел бы иметь возможность реализовать и которую я даже рассматривал в качестве этакой лакмусовой бумажки состояния проекта. До сих пор получалось не очень, но вот теперь у меня кажется есть всё необходимое.
Итак, вот допустим я много и усердно трудился над arblog и получился такой хороший движок для блога, что возникло много желающих им пользоваться, но далеко не все имеют выделенный сервер, на котором можно было бы развернуть необходимое lisp-окружение. И тогда меня может вдруг осенить - а почему бы не сделать движок многопользовательским и не составить конкуренцию Blogger? (ну мало ли какие глупые мысли порой приходят в голову).
С одной стороны я не хочу превращать arblog в "multi-authors blog engine", а с другой и дублировать функционал в новом движке тоже не хочу, а хочу воспользоваться ранее написанным и отлаженным функционалом. Собственно, ниже демонстрация того, как лёгким движением мысли можно на базе одно-пользовательского arblog создать небольшую коллективную платформу для ведения блогов.
Итогового результата я добиваюсь следующим кодом:
(: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:
(defun @multi-authors (route)
(make-instance 'multi-authors-arblog-route :target route))
Всё интересное в данном случае происходит за счёт переопределение generic-методов для multi-authors-arblog-route.
Изменяем шаблоны маршрутов:
(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.
Заставляем обработчики маршрутов и проверки дополнительных условий выполняться в контексте конкретного автора:
(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 в реальном приложении могла бы читать настройки конкретного автора из базы и формировать контекст на основе этих настроек.
Исправляем автоматическую генерацию URL, которая нарушена из-за изменения шаблонов маршрутов в коде выше:
В данном коде важную роль играет класс multi-authors-datastore, который инициализируется с помощью имени автора блога и реализует интерфейс, определённый в пакете #:arblog.policy.datastore. Поскольку у меня сейчас есть готовая реализация этого интерфейса - arblog.datastore.mongodb:arblog-mongo-datastore, то проще всего было бы создавать для каждого автора отдельную базу. Но возможно некоторым эстетам такой вариант не понравится и поэтому я немного улучшил реализацию 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", так что сообщения различных авторов могут спокойно жить в одной коллекции и не мешать друг-другу.
Для теста осталось реально добавить пару пользователей:
(arblog.policy.datastore:datastore-set-admin
(make-instance 'multi-authors-datastore :author author)
author
password))
(add-author "ivanov" "111")
(add-author "petrov" "222")
и запустить приложение:
Есть один недостаток: ссылка с названием блога, которая показывается на каждой странице, введёт к корню сайта. Что бы это исправить надо немного поправить тему, но рассказ про темы запланирован на другой раз.
Кстати, делать на практике такое с arblog у меня пока никакого желания нет, но зато я планирую использовать такой подход для restas-colorize - я хочу переработать этот модуль, добавить некоторые возможности Gist и использовать как на персональном сайте, так и на http://lisper.ru/apps/format/
P.S. Полный код здесь: https://github.com/archimag/arblog/blob/master/examples/multi-authors-blog.lisp