В первом своем посте про Grape я быстренько проскакал по основным его возможностям, за что заслуженно был засыпан упреками в излишней сжатости подачи материала. По результатам публикации прошлого поста я пообещал практический пример применения фреймворка, а также сравнительные бенчмарки.
Сегодня мы приступим к созданию примера — разработаем API, которое умеет:
— регистрировать пользователя
— активировать его по email
— обновлять данные авторизованного пользователя
— возвращать профиль авторизованного пользователя
gem install grape-gen
Если честно, я немного слукавил — писать мы ничего не будем.
После того поста я некоторое время думал — как же мне поступить? Просто так тратить время на написание сферического примера в вакууме мне не очень хотелось — хотелось, как говорится, «и рыбку съесть и косточкой не подавиться».
Поэтому я потратил немного времени и собрал гем-генератор полнофункционального приложения API на grape, назвав его незамысловато — grape-gen.
«Скелет» приложения, батарейки в комплекте
Не буду томить вас долгими рассказами о том, что нужно современному веб-приложению, кроме самого фреймворка.
Генератор «из коробки» предлагает:
— ORM (пока только mongoid)
— авторизацию (Warden + CanCanCan)
— фоновые задачи (Sidekiq)
— сообщения в реальном времени (Faye)
— интеграцию с ElasticSearch (Tire)
— загрузку файлов (Carrierwave)
— отправку email через Mandrill (MandrillMailer)
— настроенное тестовое окружение с guard+spork и гемом для документирования API с помощью тестов.
Все это склеено вместе и готово к работе.
Настоятельно рекомендуется redis, нужен как минимум для faye и sidekiq.
После gem install grape-gen в шелле станет доступна одноименная команда. Для генерации приложения выполняем:
$ grape-gen app your_app_name
В генераторе предусмотрена возможность отключать те или иные компоненты, вот хелп к команде
$ grape-gen help app
Usage:
grape-gen app APP_NAME
Options:
[--path=PATH]
[--orm=ORM]
# Default: mongoid
[--batteries=one two three] # Batteries to include
# Default: ["sidekiq", "carrierwave", "mandrill", "es", "faye"]
[--use-grape-rackbuilder], [--no-use-grape-rackbuilder]
# Default: true
Про grape-rackbuilder, включенный по-умолчанию — это мой костыль для автозагрузки файлов и перезагрузки кода в dev-окружении. В нем еще не все гладко, да и код там ужасный, но с большинством возложенных на него функций он справляется, в ближайшее время буду его латать.
Про EventMachine
В комментариях к первому посту пользователь stalkerg выразил мысль, что «без асинхронного программирования такой фреймворк имеет мало смысла».
Я считаю, что в этом есть доля правды, поэтому «из коробки» приложение предназначено к запуску под EventMachine-based сервером — Thin или Goliath.
Это означает, что все используемые в приложении «батарейки» при загрузке препарируются на предмет замены блокирующего IO на асинхронные аналоги из EventMachine, а для Carrierwave обработка изображений выносится в thread-pool через EventMachine.defer (да, у нас GIL, но даже в этом случае за одинаковое количество времени мы обрабатываем в два раза больше изображений при этом давая event-loop «вздыхать» в два раза чаще — я проверял тестами).
Ну и em-synchrony, конечно. Каждый запрос у нас выполняется в собственном fiber через Rack::FiberPool, так что никакого callback-hell.
API, предоставляемое приложением
POST /api/auth/register
email
password
display_name
POST /api/auth/approve_email
email
email_approvement_code
PUT /api/profile
display_name
avatar
remove_avatar
GET /api/profile
API у нас отдает JSON, который генерируется с помощью JBuilder и сериализуется через MultiJson+oj.
Конфигурирование приложения
Подключения к redis, elasticsearch, ключи сторонних API настраиваются в файле config/application.yml
Подключения к базе данных настраивается в config/database.yml
Настройки логгирования в config/logging.yml
Настройки sidekiq в config/sidekiq.yml
Пробный запуск
После того, как мы удостоверились в правильности наших конфигов, настало время запустить наше новоиспеченное приложение:
$ RACK_ENV=production thin start -p 9292 # Запуск сервера API
$ thin start -p 9393 -e production -R faye.ru # Запуск сервера faye
$ sidekiq -C config/sidekiq.yml -r ./config/boot_sidekiq.rb -e production # Запуск демона Sidekiq
После того, как процессы успешно стартанут, по адресу http://localhost:9292/faye будет доступна страница с примитивным faye-клиентом, подписанным на каналы /user/registered и /time
Сообщения в канал /time отправляются Sidekiq-задачей, запланированной запускаться раз в 5 секунд.
Таким образом каждые 5 секунд на страницу будет добавляться строка с временем сервера.
После регистрации пользователя и подтверждения его email в канал /user/registered добавляется сообщение с его display_name, получив которое браузер добавит строку с предложением поприветствовать нового пользователя.
Тестовое окружение
Тестовое окружение базируется на гемах RSpec 3, rspec_api_documentation. В комплекте полюбившиеся многим FactoryGirl, DatabaseCleaner и Faker
Запускается все это под Guard+Spork, плюс в тестовом окружении, как и в development, используется перезагрузка кода, что позволяет прогонять тесты достаточно быстро.
Отдельно стоит сказать про гем rspec_api_documentation — он позволяет совместить процесс написания тестов и формирования документации API.
До этого я использовал Swagger, но к сожалению он в большей степени подходит для каноничных REST API. Если у вас API больше в стиле JSON RPC, то вам будет непросто вместить свое API в описательную структуру Swagger, при этом документирование структуры ответов API доступно только для grape-entity.Вышеупомянутый гем с помощью своего DSL поверх RSpec позволяет документировать API с помощью примеров: вы описываете тестовый пример (например, валидную регистрацию пользователя), при запуске этого примера он запоминает посланный на сервер запрос, полученный ответ, url запроса и из этой информации генерирует документацию. Также есть возможность задать описание параметров и. т. д.
Вот пример:
resource "Account" do
get "/accounts" do
parameter :page, "Page to view"
# default :document is :all
example "Get a list of all accounts" do
do_request
status.should == 200
end
# Don't actually document this example, purely for testing purposes
example "Get a list on page 2", :document => false do
do_request(:page => 2)
status.should == 404
end
# With example_request, you can't change the :document
example_request "Get a list on page 3", :page => 3 do
status.should == 404
end
end
post "/accounts" do
parameter :email, "User email"
example "Creating an account", :document => :private do
do_request(:email => "eric@example.com")
status.should == 201
end
example "Creating an account - errors", :document => [:private, :developers] do
do_request
status.should == 422
end
end
end
Что дальше?
Думаю, что основную информацию, необходимую для быстрого создания своего API-приложения в этом посте я предоставил.
Гемы, использованные в проекте, достаточно хорошо документированы. Если вы считаете, что я упустил что-то важное — пишите в комментариях или ЛС, я обязательно добавлю.
К следующему посту я постараюсь подготовить более-менее объективные бенчмарки данного приложения.
Пока могу сказать, что на i5 2500K один инстанс приложения (один thread) обрабатывает ~700 запросов в секунду к POST /api/auth/register с данными существующего пользователя.
Также в планах есть добавление поддержки JRuby на Goliath-сервере (уж больно там JIT хорош) и http и in-app кеширования.
Автор: AMar4enko