новости
статьи
.программирование

November — вики-движок на Perl 6

На конференции YAPC::Europe 2008, состоявшейся в августе этого года в Копенгагене, шведы Карл Мэсак (Carl Masak) и Йохан Виклунд (Johan Viklund) представили свою разработку — вики-движок, написанный на Perl 6. Не смотря на то, что функционал системы на сегодняшний день ограничивается лишь базовыми возможностями (логин и редактирование страницы) и работает довольно медленно, с проектом обязательно стоит познакомиться, поскольку он является одним из первых реальных приложений, которые созданы на новом языке, выход которого ожидается уже не один год.

установка

Подразумевается, что вики-движок November работает под управлением Rakudo (хотя сам по себе Perl 6 не ограничивает выбор компилятора), поэтому на сервере должен быть установлен Parrot. О том, как установить Parrot и собрать компилятор, рассказано в статье Морица Ленца в этом номере журнала.

Для установки November необходимо сперва скопировать код из git-репозитория:

$ git clone git://github.com/viklund/november.git

После этого будет создан каталог november, в котором среди прочих находятся каталоги p5w и p6w. В первой находится вики-движок на Perl 5, он служит эталонным набором функций, которым должен обладать такой движок. Код собственно вики November размещен в каталоге p6w.

Сборка потребует запуска команд perl Makefile.PL и make, и попросит указать путь к каталогу, где находится Parrot:

$ cd p6w
$ perl Makefile.PL
. . .
Parrot checkout location: [~/parrot]
$ make


После этого будут скомпилированы PM-модули, при моей конфигурации вывод выглядит следующим образом:

/software/parrot/parrot /software/parrot/languages/perl6/perl6.pbc --target=pir Impatience.pm > Impatience.pir
/software/parrot/parrot /software/parrot/languages/perl6/perl6.pbc --target=pir HTML/Template.pm > HTML/Template. pir
/software/parrot/parrot /software/parrot/languages/perl6/perl6.pbc --target=pir CGI.pm > CGI.pir
/software/parrot/parrot /software/parrot/languages/perl6/perl6.pbc --target=pir Text/Markup/Wiki/Minimal.pm > Tex
t/Markup/Wiki/Minimal.pir
/software/parrot/parrot /software/parrot/languages/perl6/perl6.pbc --target=pir Wiki.pm > Wiki.pir
/software/parrot/parrot /software/parrot/languages/perl6/perl6.pbc --target=pir Test.pm > Test.pir


Этот вывод может отличаться от того, что произойдет в вашем случае.

Обратите внимание, что развитие проекта идет параллельно с добавлением новых возможностей (и исправлением ошибок) в Rakudo, поэтому перед установкой November желательно обновить версию Parrot. Например, я столкнулся (уже после того, как November успешно установился) с таким сообщением об ошибке:

Method 'exists' not found for invocant of class 'Perl6Hash', referer: http://november.perl6.r u/cgi-bin/wiki.sh

Ошибка исчезла сразу после обновления Parrot: за несколько дней до этого разработчики сообщили о том, что метод exists для проверки существования ключа хеша не был реализован, и как только он появился в новой версии Rakudo, он тут же был использован в проекте.

November представляет собой CGI-приложение, основной скрипт которого называется wiki.sh и находится в каталоге p6w. Веб-сервер следует настроить таким образом, чтобы этот файл отображался, например, на адрес /cgi-bin/wiki.sh. В конфигурации (Apache) полезно сделать запись

DirectoryIndex /cgi-bin/wiki.sh

Кроме того, CSS-файл и пара картинок, находящиеся в каталоге skin, должны быть доступны в корне сайта.

Если все установлено правильно, по URL-адресу, на котором установлен November, должна загрузиться первая страница:



Рис. 1. Главная страница November.

wiki.sh и wiki

Основной скрипт приложения, wiki.sh, который исполняется сервером в ответ на все приходящие запросы, представляет собой шелл-скрипт, который запускает Rakudo:

#!/bin/sh
PARROT_DIR=/software/parrot
exec $PARROT_DIR/parrot $PARROT_DIR/languages/perl6/perl6.pbc wiki


Компилятор получает на вход небольшой файл wiki, который написан уже на Perl 6:

#!perl6
use v6;

use CGI;
use Wiki;

my Wiki $wiki = Wiki.new;
$wiki.init();
my $cgi = CGI.new;
$cgi.init();
$wiki.handle_request($cgi);


Такая двухходовая схема, к сожалению, пока неизбежна, но в будущем, разумеется, шелл-прослойка не потребуется. Аналогичный подход я применил для сайта real.perl6.ru, о котором рассказано в статье в этом номере журнала.

Код крайне прост и логичен: создается экземпляр класса Wiki, после чего он инициализируется вывозом метода init(), а затем происходит обращение к методу handle_request(), выполняющего всю обработку запроса. Этому методу передается переменная $cgi, которая представляет собой объект типа CGI (о нем будет рассказано ниже).

класс Wiki

Определение класса Wiki находится (разумеется) в одноименном файле Wiki.pm. Обратите внимание, что во во время установки этот и другие модули были скомпилированы и сохранены в виде PIR-кода в файлах с расширением .pir. В одной из недавних доработок Rakudo компилятор научился в ответ на директиву use Module искать на диске не только исходный код в файле Module.pm, но и скомпилированный вариант Module.pir. Если PIR-версия найдена, то это существенно ускоряет работу всего приложения. Кстати, возможно предварительно скомпилировать и сам файл wiki, а в файле wiki.sh передать компилятору файл wiki.pir. Однако, на таком маленьком файле заметного прироста производительности (в несколько раз), как это происходит при компиляции больших модулей, уже не наблюдается.

Класс Wiki примечателен тем, что помимо возможностей по созданию классов в Perl 6, демонстрирует и то, как использовать и наследование (создавая роли), и композицию (создавая переменные классов, которые являются объектами определенных пользователем типов):

class Wiki does Session {
my $.template_path is rw;
my $.userfile_path is rw;

has Storage $.storage is rw;
has CGI $.cgi is rw;
. . .
}


Класс Wiki использует интерфейс, определенный ролью Session. Переменные (атрибуты) класса $.storage и $.cgi являются экземплярами,
соответственно, классов Storage и CGI.

Инициализация переменных происходит в методе init(), но в комментариях указано, что это вызвано лишь ограничениями текущей версии Rakudo. Подобные обходные пути встречаются довольно часто, и о них кратко рассказано в статье Карла Мэсака в этом номере журнала, поэтому дальше я не буду останавливаться на тех странных моментах, которые в ней упомянуты.

Обработчик запросов (он же — метод класса Wiki) выглядит так:

method handle_request(CGI $cgi) {
$.cgi = $cgi;
my $action = $cgi.param<action> // 'view';
given $action {
when 'view' {
self.view_page(); return;
}
when 'edit' {
self.edit_page(); return;
}
when 'log_in' {
self.log_in(); return;
}
when 'log_out' {
self.log_out(); return;
}
when 'recent_changes' {
self.list_recent_changes(); return;
}
}
self.not_found();
}


Выполняемое действие определяется содержанием CGI-параметра action, который читается из хеша в переменной типа CGI: $cgi.param<action>. Если этот ключ не определен, то по умолчанию выполняется действие view, что изящно позволяет записать оператор Perl 6 defined-or (//). Необходимое действие выполняет один из методов класса.

логин и логаут

Пользовательские сессии обрабатывает код, находящийся в методах роли Session. Эта роль выполняет базовые действия по созданию сессии, записи сессии нового пользователя и окончания сессии после логаута. Данные хранятся в файле data/session в виде дампа хеша. Например, при создании сессии функция new_session создает хеш, содержащий имя пользователя, и сохраняет его на диск, вызывая метод perl():

role Session {
. . .
method new_session($user_name) {
my $session_id = get_unique_id();
self.add_session( $session_id,
{ user_name => $user_name } );
return $session_id;
}
method add_session( $id, %stuff) {
my $sessions = self.read_sessions();
$sessions{$id} = %stuff;
self.write_sessions($sessions);
}
method write_sessions( $sessions ) {
my $fh = open( $.sessionfile_path, :w );
$fh.say( $sessions.perl );
$fh.close;
}
. . .
}


Проверка имени пользователя и его пароля происходит в методе log_in:

if (defined %users{$user_name}
and $password eq %users{$user_name}<plain_text>) {
. . .
}


То, что здесь выполняется проверка пароля в открытом виде, не должно смущать, поскольку авторы планируют подключить модуль Digest::MD5, который уже находится в репозитории.

Имена пользователей и их пароли (вместе с хешированной версией) хранятся в файле data/users, который содержит данные в виде хеша:

{
'johan' => {
'password' => 'hdm24HFlfrBgIX3bTsVLIQ',
'plain_text' => 'howdy'
},
'carl' => {
'password' => '3nRQWSNmQ/4jFD1w7AQMqg',
'plain_text' => 'howdy'
}
}


Этот файл считывается лаконичным методом read_users:

method read_users {
return {} unless file_exists( $.userfile_path );
return eval( slurp( $.userfile_path ) );
}


Функция slurp (попробовать функцию slurp в действии можно и в Perl 5 с помощью модуля Дамиана Конвея Perl6::Slurp.) прочитает весь файл, имя которого содержится в переменной $.userfile_path, и возвратит текстовую строку, которая затем выполняется (eval).

При логауте вызывается, соответственно, метод log_out, который удаляет сессию и очищает ключ куки session_id:

my $session_id = $.cgi.cookie<session_id>;
self.remove_session( $session_id );
my $session_cookie = "session_id=";


классы Storage и Storage::File

Экземпляр класса Wiki содержит переменую $.storage типа Storage:

has Storage $.storage is rw;


При этом определено два класса: Storage и унаследованный от него Storage::File:

class Storage {
. . .
}


class Storage::File is Storage {
. . .
}


Обратите внимание, что в переменной $.storage создается экземпляр класса Storage::File, а в комментарии в коде написано, что авторы планируют изменить код как только появится множественная диспетчеризация (ее более или менее полная поддержка ожидается до конца года):

$.storage = Storage::File.new();
$.storage.init();


Класс Storage описывает базовые действия (например, чтение страницы или сохранение внесенных изменений), которые возможно выполнить с хранилищем данных, не вдаваясь в сущность самого хранилища. При этом часть методов объявлена чисто виртуальными, и обязана быть переопределенной в производных классах (для этого используется синтаксическая конструкция Perl 6 — три точки):

method wiki_page_exists($page) { ... }
method read_recent_changes() { ... }
method write_recent_changes( $recent_changes ) { ... }
method read_page_history($page) { ... }
method write_page_history( $page, $page_history ) { ... }
method read_modification($modification_id) { ... }
method write_modification( $modification_id, $modification ) { ... }

Класс же Storage::File конкретизирует общие действия для работы с хранилищем страниц в виде набора файлов. Сами файлы находятся в каталоге data. Например, вот метод для сохранения истории изменений:

method write_page_history( $page, $page_history ) {
my $file = $.content_path ~ $page;
my $fh = open($file, :w);
$fh.say( $page_history.perl );
$fh.close;
}


класс CGI

Особенность разработки проектов на Perl 6 сегодня — необходимость самостоятельно создавать многие модули, которые уже стали стандартными в Perl 5. Для November написан простой класс CGI. Работа с параметрами запроса и куками реализована с помощью методов parse_params, parse_keywords, unescape и eat_cookie. Сами параметры хранятся в соответствующих хешах:

has %.param is rw;
has %.cookie is rw;


Читателю предлагается самостоятельно разобраться, что происходит внутри этих методов, поскольку они содержат большое число обходных путей для решения задач, на решение которые пока не способен Rakudo.

Интересно обратить внимание на то, как читается входной поток данных перед передачей их в метод parse_params:

my $input = $*IN.slurp();
self.parse_params(%params, $input)


Глобальный десктиптор $*IN привязан ко входному потоку, а вызов метода slurp считывает все имеющиеся на входе данные.

Кроме того, класс CGI содержит методы send_response и redirect, которые печатают HTTP-ответ в соответствующем формате.

класс HTML::Template

HTML-код страниц формируется с участием модуля HTML::Template (написанного специально для проекта).

Генерирование происходит в три этапа: создание экземпляра класса HTML::Template, подготовка нужных параметров и вызов метода output. Например, как в этом фрагменте функции edit_page:

my $template = HTML::Template.new(
filename => $.template_path ~ 'edit.tmpl');

$template.param('PAGE' => $page);
$template.param('TITLE' => $title);
$template.param('CONTENT' => $old_content);
$template.param('LOGGED_IN' => True);

$.cgi.send_response(
$template.output(),
);


Шаблоны HTML-кода хранятся в каталоге skin, и содержат директивы вида <TMPL_XX...>, например, для вывода заголовка:

<h1><TMPL_VAR NAME=TITLE></h1>

Реализован даже условный оператор и циклы:

<ul id="toolbar">
<li><a href="?page=<TMPL_VAR NAME=TITLE>&action=edit">Edit</a></li>
<TMPL_IF NAME=LOGGED_IN>
<li><a href="?action=log_out">Log out</a></li>
<TMPL_ELSE>
<li><a href="?action=log_in">Log in</a></li>
</TMPL_IF>
</ul>

. . .

<ul>
<TMPL_LOOP NAME=CHANGES>
<li>
<TMPL_VAR NAME=PAGE> was changed on
<TMPL_VAR NAME=TIME> by
<TMPL_VAR NAME=AUTHOR>
</li>
</TMPL_LOOP>
</ul>


В чтении шаблона с диска вновь участвует полезный метод slurp:

$template = slurp( $.filename );


Работу по разбору шаблона, поиску и выполнению директив выполняет метод serialize, где в цикле анализируются найденные теги и производятся соответствующие действия:

method serialize($text is rw) {
my @loops;
while ( $text ~~ / '<TMPL_' (<alnum>+) ' NAME=' (\w+) '>' / ) {
my $directive = $0;
my $name = $1;
. . .
if $directive eq 'LOOP' {
. . .
}
elsif $directive eq 'IF' {
. . .
}
else { # it's TMPL_VAR
$text = $text.subst(
/ '<TMPL_VAR NAME=' \w+ '>' /,
$value
);
}
}
return $text;
}


класс Text::Markup::Wiki::Minimal

Этот класс появился в проекте совсем недавно и изучать его — одно удовольствие.

При формировании HTML-кода страницы метод view_page класса Wiki передает объекту шаблона параметр CONTENT, который заполняется результатом работы метода format:

$template.param(
'CONTENT' =>
Text::Markup::Wiki::Minimal.new.format(
$.storage.read_page($page),
{ self.make_link($^page) }
));


Файл Minimal.pm содержит определение класса и грамматики, описывающий вики-синтаксис.

Сама грамматика довольна компактна и позволяет легко вносить изменения:

grammar Text::Markup::Wiki::Minimal::Syntax {
token paragraph { ^ [<heading> || <parchunk>+] $ };
token heading { '==' <parchunk>+ '==' };
token parchunk { <twext> || <wikimark> || <metachar> || <malformed> };
token twext { [ <.alnum> || <.otherchar> || <.whitespace> ]+ };
token otherchar { <[ !..% (../ : ; ? @ \\ ^..` {..~ ]> };
token whitespace { ' ' | \n };
token wikimark { '[[' <twext> ']]' };
token metachar { '<' || '>' || '&' || \' };
token malformed { '[' || ']' }
}


Токен paragraph описавает содержимое абзаца любого вики-документа. Именно с этим токеном происходит сопоставление очередного абзаца текста (это происходит в методе format класса Text::Markup::Wiki::Minimal):

if $par ~~ Text::Markup::Wiki::Minimal::Syntax::paragraph {
. . .
}
else {
$result = '<p>Could not parse paragraph.</p>';
}


При удачном сопоставлении выполняются соответствующие изменения текста, например, форматирование загловка:

if $/<heading> {
my $heading = $/<heading><parchunk>[0];
$heading = $heading.subst( / ^ \s+ /, '' );
$heading = $heading.subst( / \s+ $ /, '' );
$result = "<h1>$heading</h1>";
}


в заключение

Проект November постоянно развивается, поэтому часть описанного здесь может устареть даже к моменту публикации материала. Однако многие изменения отражают изменения, происходящие с Rakudo, причем эта связь двунаправлена: разработчики November, сталкиваясь с неверным поведением компилятора, сообщают об этом разработчикам Rakudo, и после реализации (часто это происходит в течение нескольких дней) воплощают новую конструкцию, которая по духу более соответствует Perl 6.

ссылки

Демонстрационная версия November расположена по адресу http://november-wiki.org.

Исходный код — в репозитории http://github.com/viklund/november/.

Кроме того, существуют список рассылки november-wiki@googlegroups.com и IRC-канал #november на сервере irc.freenode.org.

Видеозапись выступления авторов проекта можно посмотреть на странице http://yapc.tv/2008/ye/lt/lt2-01-masak-vilkund-november/.



Андрей Шитов, andy@shitov.ru
обсудить статью
© сетевые решения
.
.