
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 =).
Заключение
Можно ещё много чего написать, но слегка устал :)
В принципе, всё пишется достаточно быстро и прикольно. Это мой первый работающий код на эрланге. Пока-что всё нравится.
Примечание: в коде статьи явно дофига ошибок, недописанных гардов и прочих мелочей. Прошу сильно не пинать, пишу на эрланге второй день.
Пишите в личку, поправлю статью.
