REST API на Sinatra

Мы научились работать с чужими API, настало время научиться писать свое собственное.

Sinatra - это уже нечто большее, чем библиотека, это фреймворк.

Главная отличительная особенность заключается в том, что уже не разработчик пишет как будет работать его программа, а фреймворк, предоставляя разработчику возможности для конфигурации своего собственного поведения.

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

Вообще если говорить про сетевой стек, то его можно разделить на 3 модуля:

  • Rack - контракт или интерфейс, описывающий как должен вести себя сервер. Rack не содержит в себе никакого кода для работы сети, он лишь говорит, что "если мы дернем метод А, то получим Б", в то время как реализация этого метода А, каким образом мы будем получать Б, это уже задача другого компонента-реализации

  • Rack реализация - по другому еще называют Rack-совместимый сервер. Это уже готовая библиотека с реализацией сетевых протоколов, которые являются реализацией методов Rack. Сюда также входит логика организации многопоточных обработок, чтобы наш сервер мог обрабатывать не по 1 запросу от пользователя, а десятки, сотни или даже тысячи запросов от пользователей. К таким реализациям можно отнести гемы thin, puma, unicorn, falcon и другие

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

Теперь давайте добавим в наш Gemfile следующие гемы:

  • thin - простая реализация Rack сервера

  • sinatra - наш фреймворк для удобной работы с Rack сервером

После добавления гемов в Gemfile вызовите bundle install

Теперь создадим самый простой сервер с помощью нашего фреймворка:

require 'sinatra'

# sinatra предоставляет метод для конфигурации
# который принимает блок кода, внутри которого мы передаем необходимые настройки
# подробнее о конфигурации https://sinatrarb.com/configuration.html
configure do
  # set - метод принимающий 2 аргумента: ключ конфигурации и значение
  # в данном случае опускаются скобки для более красивого синтаксиса
  # указываем, что в качестве реализации rack сервера будет thin
  # если данную настройку не указывать, то sinatra будет использовать первый попавшийся сервер из устанолвенных гемов
  set :server, :thin
  # указываем какой порт наше приложение должжно слушать
  # если дпнную настройку не указывать, то будет использоваться порт 4567
  set :port, 8000
end

# Теперь мы указываем что:
# - наш сервер умеет обрабатывать GET метод
# - по относительному адресу - /, то есть корневой раздел
# Такие методы в простонародье называют 'ручкой'
get '/' do
  'hello world !' # А ответом на этот запрос будет строка Hello world !
end

запустим наш .rb файл, если все хорошо, то увидим сообщение Listening on localhost:8000, CTRL+C to stop

Теперь протестируем через клиент в VSCode, дернув нашу новосозданную ручку. В адрес ресурса указываем метод GET и путь localhost:8000, где localhost это синоним для локального IP адреса, а 8000 - порт, который слушает наше приложение.

Если в ответ на ваш запрос вы получили строку Hello World !, значит можно вас поздравить ! Вы запустили свое первое серверное приложение.

Теперь разберемся как нам научиться принимать части относительного пути. Создадим новую 'ручку':

Теперь попробуем отправить запрос по адресу localhost:8000/person/john/23 и получим ответ:

Hello, john. You are 23 y.o.

Отлично.

Таким образом, если в отностельном пути мы можем изменять определенные части и наше приложение будет уметь их распозновать.

А что на счет параметров, которые передаются после вопросительного знака ? Sinatra по умолчанию считает все такие параметры необязательными, поэтому они нигде не объявляются в коде. При этом сами параметры мы можем получить точно так же как и куски пути. Например:

А теперь если мы отправим запрос на адрес localhost:8888/person/John/23?country=Russia, то мы увидим сообщение:

Hello, John from Russia. You are 23 y.o.

Хотя ни в каком пути мы параметр country не объявляли

Все, конечно, здорово, но хорошо бы нам научиться отдавать JSON. Возможно вы уже догадались, что нужно сделать.

Для начала создадим объект-хранилище. Такие объекты принято называть DTO (Data Transfer Object) и если дословно перевести, то сразу становится понятно, что такие объекты выполняют только одну функцию - транспортировка данных.

Теперь добавим зависимости в файл нашего сервера:

После чего соберем класс Person и вернем его в качестве ответа в формате JSON

Снова отправим запрос на localhost:8888/person/John/23?country=Russia и уже в ответ получим JSON:

Структура проекта

В реальной жизни никто не будет писать сервера со всеми ручками в одном файле, поэтому с точки зрениях структуры проекта принято располагать ручки в отдельных классах (их еще называют роутеры Routers, а сами ручки роуты Route). При этом отдельный роутер отвечает только за свою отвественность.

Пример: У нас есть интернет-магазин с товаром. Какие области отвественности могут быть у магазина ? Товар, заказ, личный кабинет и тд. Как будет выглядить наша структура проекта:

При этом сам роутер будет выглядеть следующим образом:

в то время как routes/init.rb будет служить файлом для объединения наших роутеров

А можно в динамике с помощью Dir вытащить пути до всех файлов в папке с файлом init.rb и автоматически их импортировать

Файл server.rb содержит в себе класс который будет хранить в себе конфигурации для нашего Sinatra приложения, а также за счет run! if app_file == $PROGRAM_NAME мы сможем запускать наше приложение простым вызовом ruby path/to/server.rb.

файл config.ru является конфигурационным файлом для утилиты rackup, а не через вызов ruby. Зачем ? Чтобы если мы захотим поменять сервер-имплементацию Rack спецификации или изменить порт для запуска приложения нам бы не пришлось менять исходный код, а всего лишь поменять значения в команде запуска

И теперь простой командой запусаем наш сервер rackup путь/до/config.ru -s thin -p 8000. Как видим указание какой Rack совместимый сервер использовать, а также порт прослушиваемый приложением переехал из кода в командную строку.

Возможно у вас возник вопрос: А почему мы называем все классы одинаково и нарушаем правило имя_файла == ИмяКласса ? На самом деле это хитрость метапрограммирования из урока 9_professional/1_metaprogramming, которая позволяет за счет одинакового названия классов их смешать в один целый и большой класс.

Скорее всего эта часть урока вам могла показаться чересчур сложной за счет манипуляций с файлами и динамического импорта этих файлов, но если посидеть и разборать код по строчно, то все встанет на свои места через какое-то время изучения. Цель данных примеров - показать не только как организовать структуру проекта, но и какие сложные и в то же время простые вещи нам позволяет делать Ruby, так как при изучении Rails в дальнейшем нам часто придется сталкиваться с такими хитростями, которые реализовывает сам фреймворк.

Задание weather_info

Внимание! это задание необходимо разместить в директории модуля 11_network/weather_info

Сделайте из вашего консольного приложения серверное на базе фреймворка sinatra где:

  • адрес вашей ручки будет weathers, которая:

    • принимает страну в виде параметра запроса country

    • принимает город в виде параметра запроса city

  • если один из параметров не заполнен, то выбросить 400 ошибку с указанием какая переменная не была передана

  • Вернуть ответ в формате JSON

Пример запроса:

  • GET localhost:8000/weathers?country=Russia&city=Moscow

  • GET localhost:8000/weathers?city=Astana

400 Bad Request - param country is empty

Рекоммендации:

  • Для разделения логики клиентов и севеерной логики, разместите классы клиентоав в отдельном каталоге clients

Дополнительный материал

Last updated