Ruby On Rails

Теория: Rack

Rack – это интерфейс для взаимодействия веб-сервера с HTTP-запросами. У него есть несколько функций:

  • Стандарт интерфейса веб-сервера. Rack определяет, как серверы обрабатывают запросы и взаимодействуют друг с другом. Позволяет использовать различные серверы для запуска приложений

  • Каркас для Middlewares. Rack работает как каркас для middleware, которые обрабатывают запросы по конвейерному принципу.

  • Библиотека. Rack содержит вспомогательные функции для более быстрой разработки. Используется в таких фреймворках, как Sinatra и Rails.

Понимание Rack важно для разработки на Sinatra и Rails, поскольку они подчиняются его стандартам.

Приложение Rack

Чтобы запустить Rack, необходимо импортировать библиотеку, вызвать хендлер и применить метод run(), передавая объект, который содержит метод call(). Приложение запускается командой ruby app.rb

# app.rb
require 'rackup'

class MyApp
  def call(_env)
    [200, {'Content-Type' => 'text/html'}, ["Hello"]]
  end
end

Rackup::Handler::WEBrick.run MyApp.new, :Port => 3000

Метод call() должен вернуть массив из трех элементов:

  • Статус ответа.
  • Хедеры в виде хэша.
  • Тело ответа. Тело ответа обычно представляет собой массив строк

Разработчики Rack также создали консольную утилиту под названием rackup. Эта утилита ищет файл с именем config.ru в текущей папке и запускает сервер на порту 9292. Если в config.ru указано приложение, то при отправке GET-запроса на порт 9292 сервер вернет тело ответа, указанное в качестве третьего элемента в конфигурации приложения.

# config.ru
class MyApp
  def call(_env)
    [200, {'Content-Type' => 'text/html'}, ["Hello"]]
  end
end

run MyApp.new

Запуск может выполняться одной из команд в зависимости от используемого сервера

# автоматически ищется файл config.ru
rackup
rackup -s thin
thin start
puma
unicorn
passenger start

Проверяем работу приложения

# Запустили сервер
rackup

# В другом терминале выполнили запрос
curl -X GET localhost:9292
# => Hello!

Middlewares

Middleware – это фильтры запросов, которые обрабатывают информацию о запросе и передают её следующему компоненту.

Применение middleware:

  • Авторизация

    Middleware может управлять доступом, включая встроенные решения для basic-auth

  • Мониторинг

    Можно отслеживать количество запросов и их время выполнения

  • Логирование

    Подходит для записи работы приложения, особенно на уровне системы.

  • Сериализация

    Поддерживает передачу данных, включая динамические переменные.

  • Роутинг

    Доступ к параметрам запроса позволяет определять, как следует обрабатывать запрос.

  • Бизнес-логика

    Может реализовываться через вызов сервисных объектов в middleware.

Запрос поступает в приложение и передается через цепочку middleware, каждая из которых обрабатывает определенную логику. Ответ формируется последней middleware.

Каждый компонент middleware должен возвращать три элемента: статус, хедеры и тело ответа.

Ниже пример приложения, содержащее миддлвар

# config.ru
class MyMiddleware
  def initialize(app1)
    @app1 = app1
  end

  def call(env)
    puts 'middleware_before'
    # env содержит запрос
    status, headers, body = @app1.call(env)
    puts 'middleware_after'
    request = Rack::Request.new(env)
    if request.path == '/'
      case request.request_method
      when 'GET'
        [status, headers, body]
      when 'POST'
        [201, headers.merge({'x-created' => 'True'}), ['Item was successfully created']]
      end
    else
      [404, {}, ["Not Found"]]
    end
  end
end

class App
  def call(env)
    puts 'app_run'
    [200, {}, ["success"]]
  end
end

# Добавляется миддлвар
use MyMiddleware
# Запуск приложения
run App.new

Пример приложения, которое обрабатывает POST запрос:

require 'json'

class MyMiddleware
  def initialize(app1)
    @app1 = app1
  end

  def call(env)
    status, headers, body = @app1.call(env)
    request = Rack::Request.new(env)

    body = {
      path: request.path,
      verb: request.request_method,
      ip: request.ip,
      cookies: request.cookies,
      params: request.params,
      body: JSON.parse(request.body.read)
    }

    [200, {}, [body.to_json]]
  end
end

class App
  def call(env)
  end
end

use MyMiddleware
run App.new

Запуск и выполнение запроса:

rackup
curl -x POST localhost:9292/users?sort=desc -d "{\"login\":\"admin\",\"password\":\"password\"}"

Пример Middleware, который обрабатывает предыдущий ответ от приложения и добавляет к телу новую информацию. В этом примере добавляется к телу ответа добавляется текущее время

class TimeStamp
  def initialize(app)
    @app = app
  end

  def call(env)
    # Вызываем приложение и получаем его ответ
    prev_response = @app.call(env)
    status, headers, prev_body = prev_response

    # Если статус не 200, возвращаем предыдущий ответ без изменений,
    # чтобы не обрабатывать не успешные ответы
    return prev_response if status != 200

    # Получаем текущее время в формате строки
    current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")

    # Добавляем текущее время к предыдущему телу ответа
    next_body = prev_body.push('</br>', "Текущее время: #{current_time}")

    # Возвращаем новый ответ с добавленным временем
    [status, headers, next_body]
  end
end

В этом примере миддлвара Timestamp принимает на вход приложение Rack и перехватывает его ответ. Если статус ответа равен 200 (успешный ответ), миддлвара добавляет к телу ответа текущую дату и время в виде строки.

Приложение с базовой авторизацией

use Rack::Auth::Basic do |username, password|
  username == 'admin' && password == 'password'
end

class App
  def call(env)
    puts env["HTTP_AUTHORIZATION"]
    [200, {'Content-Type' => 'text/html'}, ["You have been successfully logged in."]]
  end
end
run App.new

Запуск и запрос с авторизацией

rackup
curl -u admin:password -i http://localhost:9292

Тестирование Rack-приложений

Запуск выполняется командой ruby test.rb

# test.rb
require 'minitest/autorun'
require 'rack/test'

class MyApp
  def call(env)
    [200, {'X-success' => true}, ["Success response"]]
  end
end

describe "MyApp" do
  include Rack::Test::Methods

  def app
    MyApp.new
  end

  it 'check response status' do
    get '/'
    assert last_response.ok?
  end

  it 'check response headers' do
    get '/'

    assert_equal last_response.headers, {'X-success' => true}
  end

  it 'check response body' do
    get '/'
    assert_equal last_response.body, "Success response"
  end
end

#
# https://www.rubydoc.info/github/brynary/rack-test/Rack/Test/Methods
# https://devhints.io/rack-test

Мидлвары Rack

Rack предоставляет различные готовые middleware для улучшения функционала, вот некоторые из них:

Сам Rack поставляется со следующим промежуточным программным обеспечением:

  • Rack::Files для раздачи статических файлов.
  • Rack::Events для создания удобных хуков при получении запроса и отправке ответа.
  • Rack::Head для возврата пустого тела для HEAD-запросов.
  • Rack::Lock для сериализации запросов с помощью мьютекса.
  • Rack::Reloader для перезагрузки приложения, если были изменены файлы.
  • Rack::Runtime для включения в заголовок ответа времени, затраченного на обработку запроса.
  • Rack::ShowException для перехвата необработанных исключений и представления их в удобном виде.
  • Rack::MethodOverride для изменения метода запроса на основе переданного параметра.

Хелперы Rack

Rack предоставляет множество хелперов:

  • Rack::Request - обеспечивает разбор строки запроса и работу с несколькими частями.
  • Rack::Response - для удобной генерации HTTP-ответов и обработки cookie.
  • Rack::MockRequest и Rack::MockResponse для эффективного и быстрого тестирования Rack-приложений без реальных HTTP-сессий.
  • Rack::Directory - для раздачи файлов в директории.
  • Rack::MediaType - для разбора заголовков типа содержимого.
  • Rack::Mime - для определения типа содержимого на основе расширения файла.

Sinatra

Sinatra — это легковесный веб-фреймворк, построенный на основе Rack. Он предлагает простой и элегантный способ создания веб-приложений, предоставляя разработчикам возможность быстро разрабатывать RESTful API и небольшие веб-приложения.

Sinatra имеет минималистичный синтаксис, что делает его идеальным выбором для небольших проектов и прототипов. Фреймворк позволяет добавлять только необходимые компоненты, что позволяет создавать приложения, максимально соответствующие вашим требованиям. Также он поддерживает использование middleware, что позволяет комбинировать различные библиотеки и улучшать функциональность вашего приложения.

Пример простого приложения на Sinatra

Создаем директорию проекта:

mkdir sinatra-app
cd sinatra-app
bundle init

Добавляем зависимости в Gemfile:

source 'https://rubygems.org'

gem 'puma'
gem 'rackup'
gem 'sinatra'

Создаем app.rb - точку входа в наше приложение:

require 'sinatra'

get '/' do
  'Hello, world!'
end

get '/hello/:name' do
  "Hello, #{params['name']}!"
end

Запускаем:

ruby app.rb
== Sinatra (v4.1.1) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.5.0 ("Sky's Version")
* Ruby version: ruby 3.3.4 (2024-07-09 revision be1089c8ec) [x86_64-linux]
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 138933
* Listening on http://127.0.0.1:4567
* Listening on http://[::1]:4567
Use Ctrl-C to stop

Наше приложение будет доступно по адресу http://localhost:4567, и на странице будет выведена строка Hello, World!

Заключение

В этом уроке мы познакомились с основами Rack, его компонентами и концепцией middleware. Мы также рассмотрели Sinatra как легковесный фреймворк, построенный на основе Rack.

Рекомендуемые программы