www.youtube.com/watch?v=NisCkxU544c
Посмотрев презентацию Юрия (yrashk) с Erlang Conference о веб-фреймворках в эрланге я заинтересовался и решил сделать сайтик не на node.js, как намеревался изначально, а используя какой-нибудь эрланговский фреймворк.
В итоге я наткнулся на фреймворк Chicago Boss. Про сам фреймворк я слышал и раньше, но пользоваться пока не довелось.
Что нам даёт Босс? В общем, они позиционируют себя как «типа рельсы, но на эрланге». С рельсами я, кстати, тоже дела совсем не имел, так что мне было всё равно. Но с MVC имел дело, т.к. на работе у нас сплошной ASP.NET MVC2. Так что базовые концепции понятны.
Имеется в наличии BossDB; это, фактически, ActiveRecord, написанный с использованием параметризированных модулей, поддерживающий разные backends. Memory, Mnesia, MongoDB, MySQL, Postgres. По заверениям разработчиков для поддержки новой базы нужно написать порядка трехсот строк кода.
Имеются Django Templates, с почти полным покрытием всех фич, фильтров и прочего.
Есть и другие штуки, с которыми я пока особо не игрался…
Инсталляция
С инсталляцией на убунте была небольшая заморочка, которая лечится скачиванием старой версии эрланга и копированием оттуда одного заголовочного файла (lib/kernel/src/inet_dns.hrl), котрый в убунтовской поставке почему-то отсутствует; ещё пришлось поставить пакет erlang-dev.
После этого делается
make
make app NewSite
cd ../NewSite
Начинаем разбираться...
В принципе, есть неплохой туториал An Evening with Chicago Boss. Но он малость устарел, и слегка не соответствует реалиям последних версий.
Задачи
- Написать пару страничек «для всех»
- Написать авторизацию
- Написать редактор профиля пользователя
Структура проекта
/controller
/view
/model
/log
/static
/test
с этими тремя всё ясно
/admin - небольшая админка, можно смотреть/создавать записи для имеющихся моделей, напишу побольше дальше
/ebin - скомпилированные файлы, для запуска в рабочем режиме
/lang - файлы языковых ресурсов. Босс сам шерстит все шаблоны на предмет переводимых строк
/lib - для любого кода, который не модель и не контроллер. У меня там код проверки авторизации
/mail - у босса есть возможность вешать события на модели, и автоматом рассылать уведомления об изменениях
Если запустить проект через ./start-dev.sh — то босс автоматом будет делать горячую перезагрузку всего, так что можно разрабатывать не перезапуская сервер, что очень удобно.
Модель
Model API
Модель — параметризированный модуль эрланга, штука специфическая, я про это мало знаю. Пока просто пользуюсь как есть.
Например модель пользователя
-module(user, [Id, FirstName, LastName, Email, PasswordHash, CreateTime]).
-compile(export_all).
Вот это — уже модель. У неё есть свойства, а так-же автоматически сгенерированные методы доступа к полям. Свойства, которые заканчиваются на Id или Time — особые. Одни — для создания ссылок и зависимостей между объектами, вторые будут принимать только кортежи в формате, который отдаёт erlang:now()
После того, как код модели был сохранён в /models/user.erl — в админке появится доступ к списку текущих моделей, и документации к классу.
У модели можно создавать методы валидации, но пока мы это пропустим.
BossDB
BossDB API
Это API для работы с базой данных.
Босс хранит все записи с Id следующего вида <model_atom>-<unique_id>, где model_atom — это атом, описанный в модуле модели :), а unique_id — это числовой идентификатор, который не повторяется даже для разных моделей, так что если у вас есть модели user и article, user-1 и article-1 существовать не смогут. Учитывая, что числа в эрланге ограничены объёмом оперативной памяти — об переполнении id можно не беспокоиться :).
Особенно интересны операции поиска. У них имеется короткая форма, записывая математическими символами, типа ∈, ∉, ≁ и прочими.
Выборку можно осуществлять передавая строковой Id записи. Т.к. он префиксирован именем модели — неразберихи не будет.
User = boss_db:find("user-1").
Контроллер
Controller API
Контроллер — тоже параметризированный модуль. Но параметр у него только один. Это поле, в котором будет находиться http запрос. У запроса есть методы для доступа к полям формы (в случае POST) и заголовкам.
-module(user_controller, [Req]).
-compile(export_all).
index('GET', []) ->
ok.
Вот простой контроллер, который просто отрендерит шаблон по умолчанию при GET запросе к адресу /user/index
Можно написать контроллер, выводящий юзера по его Id, а так-же понимающий разные методы выдачи.
Парсинг URL осуществляется следующим образом:
-module(user_controller, [Req]).
-compile(export_all).
index('GET', []) ->
{ok, [{users, boss_db:find(user, [])}]};
index('GET', ["id", Id, "method", Method]) ->
case boss_db:find(Id) of
{error, Reason} ->
boss_flash:add(Req, error, "Invalid User", "User not found"),
ok;
{user, User} ->
case Method of
"json" ->
{json, User} %%тут, возможно, не сработает - это просто иллюстрация
_ ->
{ok, [{user, User}]}
end
end.
Что-же тут происходит?
Если к нам приходит пустой запрос — то найти все объёкты модели User, и отрендерить шаблон по умолчанию, передав в него список параметров, одним из которых будет users, содержащий всех пользователей.
Если у запроса есть параметры id & method — попытаться найти пользователя по Id, в случае неудачи создать boss_flash с сообщением об ошибке и отрендерить шаблон.
Если пользователь найден то делать следующее — если метод содержит строку «json» — вернуть данные как JSON объект. Для любого другого метода — отрендреить HTML шаблон с данными пользователя.
Шаблон
Template API
используется система шаблонов Django, через библиотеку ErlyDTL
Например вот шаблон для нашей страницы /user
{{boss_flash}} <!-- в этой переменной все созданные сообщения об ошибке, которые удалятся после рендеринга -->
{% if users %}
<h1>{% trans "User List" %}</h1>
{% for user in users %}
{% trans "First Name" %} : {{ user.first_name }}</br>
{% trans "Last Name" %} : {{ user.last_name }}</br>
<a href="/user/id/{{user.id}}/method/html">{% trans "View" %}</a>
<a href="/user/id/{{user.id}}/method/json">{% trans "JSON" %}</a>
{% endfor %}
{% endif %}
{% if user %}
<h1>{% trans "User Info" %}</h1>
{% trans "First Name" %} : {{ user.first_name }}</br>
{% trans "Last Name" %} : {{ user.last_name }}
{% endif %}
Функция trans автоматом будет делать перевод строк. Так-же все строки для перевода автоматом выбираются из файлов шаблонов и попадают в веб-интерфейс перевода в админке. Файлы — обычные *.po
boss_flash это спец-конструкция для вывода временных сообщений. Не нужно париться с сессиями или передачей сообщения через параметры или скрытые поля. Очень удобно. Идентичный механизм используется в сообщениях валидаторов в ASP.Net MVC.
Авторизация
Авторизация сделана хитро. В каждом контроллере можно написать функцию before_/1, которая получает имя action и для него должна вернуть либо кортеж {ok, AdditionalParameters}, либо {redirect, "/url/to/redirect"}.
Обычно она хранится в отдельном модуле в папке /lib
-module(auth_lib).
-export(require_authentication/1).
require_authentication(Req) ->
case boss_session:get_session_data(Req, principal) of
{error, Reason} ->
{redirect, "/login"};
PrincipalId ->
case boss_db:find(PrincipalId) of
{error, Reason} ->
boss_flash:add(Req, error, "User not found", Reason),
{redirect, "/error"};
{user, Principal} ->
{ok, Principal} % Principal - будет третьим параметром у action в контроллере
end
end.
Для того чтобы воспользоваться этим методом, контроллер должен иметь методы с тремя параметрами
-module(protected_controller, [Req]).
-compile(export_all).
before_("public") -> ok;
%% адрес /protected/public можно отдавать без авторизации %
before_(_) -> auth_lib:require_authentication(Req).
%% всё остальное нужно авторизировать %
index(Req, [], Principal) ->
{ok, [{principal, Principal}]}.
public(Req, [])
ok.
Если аутентикация прошла успешно, то в третий параметр будет передан текущий юзер. Иначе будет произведена переадресация на /login.
Если юзер был аутентицирован, но из базы пропала запись — будет произведена переадресация на /error, и добавлено сообщение об ошибке.
Теперь нужно написать контроллер для проверки.
-module(login_controller, [Req]).
-compile(export_all).
index('GET', []) ->
ok;
index('POST', []) ->
Email = Req:post_param("email"),
PasswordHash = erlang:md5(Req:post_param("password")),
case boss_db:find(user, [ email = Email, password_hash = PasswordHash], 1) of
{user, Principal} ->
boss_session:set_session_value(Req, principal, Principal.id),
{redirect, "/protected"};
{error, Reason} ->
boss_flash:add(Req, error, "Invalid User"),
ok
end.
Примечание, знак 'равно' в выражении это не (ASCII =), а (UTF-8 =).
Заключение
Можно ещё много чего написать, но слегка устал :)
В принципе, всё пишется достаточно быстро и прикольно. Это мой первый работающий код на эрланге. Пока-что всё нравится.
Примечание: в коде статьи явно дофига ошибок, недописанных гардов и прочих мелочей. Прошу сильно не пинать, пишу на эрланге второй день.
Пишите в личку, поправлю статью.