> For the complete documentation index, see [llms.txt](https://evgenii-afanasev.gitbook.io/ruby-course/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://evgenii-afanasev.gitbook.io/ruby-course/11_network/3_sinatra.md).

# 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`

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

```ruby
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 !`, значит можно вас поздравить ! Вы запустили свое первое серверное приложение.

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

```ruby
# :value позволяет нам делать наш относительный путь более эластичным
get '/person/:name/:age' do
  # в качестве ключа в params мы передаем название, которое указали в пути
  name = params[:name]
  # помимо символа мы можем использовать и обычную строку
  age = params['age']
  "Hello, #{name}. You are #{age} y.o."
end
```

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

`Hello, john. You are 23 y.o.`

Отлично.

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

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

```ruby
# :value позволяет нам делать наш относительный путь более эластичным
get '/person/:name/:age' do
  # в качестве ключа в params мы передаем название, которое указали в пути
  name = params[:name]
  # помимо символа мы можем использовать и обычную строку
  age = params['age']
  country = params[:country]
  "Hello, #{name} from #{country}. You are #{age} y.o."
end
```

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

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

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

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

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

```ruby
require 'json'

class Person
  def initialize(name, age, country)
    @name = name
    @age = age
    @country = country
  end

  def to_json(*args)
    {
      'age':     @age,
      'name':    @name,
      'country': @country
    }.to_json(args)
  end
end
```

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

```ruby
require 'json'
require 'sinatra'
require_relative 'person'
```

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

```ruby
get '/person/:name/:age' do
  age = params[:age]
  name = params[:name]
  country = params[:country]
  content_type :json # укажем в HTTP ответе заголовок content-type: application/json
  Person.new(name, age, country).to_json
end
```

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

```json
{
  "age": "23",
  "name": "John",
  "country": "Russia"
}
```

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

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

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

```
routers/
  init.rb
  orders.rb
  products.rb
  accounts.rb
server.rb
config.ru
```

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

```ruby
# так как зависимость `sinatra` служит для объединения все своих зависимостей в одном месте
# то мы будет использовать более точечную завимость `sinatra/base`
# что позволит нам избежать добавления лишней функциональности
require 'sinatra/base'

# Наследуясь от Base класса мы получаем минимальный функционал для нашего роутера 
class Server < Sinatra::Base

  get 'orders/:id' do
    # какая-то логика
  end

  get 'orders' do
    # какая-то логика
  end

  delete 'orders/:id' do
    # какая-то логика
  end

  post 'orders' do
    # какая-то логика
  end
end
```

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

```ruby
require_relative 'orders'
require_relative 'products'
require_relative 'accounts'
# и так далее
```

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

```ruby
# получаем относительный путь до нашего файла от места запуска программы
current_file = __FILE__ # В моем случае это 11_network/3_shop/routes/init.rb
# отбрасываем имя файла и оставляем только путь до папки с файлами init.rb, orders.rb и тд
current_dir = File.dirname(current_file) # В моем случае это 11_network/3_shop/routes

# с помощью несложной игры со строкой мы получаем цикл по всем файлам внутри директории routes
Dir["#{current_dir}/*.rb"].each do |file|
  # так как require может работать только с полным путем до файла
  # то мы получаем полный путь от корня до каждого файла находящегося в директории routes 
  # в моем случае это будет:
  #   /home/user/11_network/3_shop/routes/init.rb
  #   /home/user/11_network/3_shop/routes/orders.rb
  #   /home/user/11_network/3_shop/routes/products.rb
  #   /home/user/11_network/3_shop/routes/accounts.rb
  #   и тд 
  full_path_file = File.expand_path(file)
  # если название файла в текущей итерации не равно названию файла из которого запускается этот скрипт
  # значит мы его импортируем как зависимость
  # иначе если мы из init.rb будем импортировать init.rb, который будет импортировать init.rb и тд
  require full_path_file unless file == current_file
end
```

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

```ruby
require 'sinatra/base'

class Server < Sinatra::Base
  # вызываем метод run!, который будет запускаться, 
  # если путь до файла в команде `ruby path/to/server` будет равно пути до этого файла
  # в таком случае этот метод не будет вызываться если мы его просто импортируем в другом месте 
  run! if app_file == $PROGRAM_NAME
end
# После того как наше приложение настроено, происходит импорт файла init, 
# который в свою очередь импортирует все роутеры 
require_relative 'routers/init'
```

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

```ruby
# импортируем наш application.rb файл
require File.expand_path('application', File.dirname(__FILE__))

# указываем rackup что ему нужно запустить для работы
run Application
```

И теперь простой командой запусаем наш сервер `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`

```json
{
  "country": "Russia",
  "city": "Moscow",
  "created_at": "2023-03-23T09:22:11Z",
  "temperature_c": 1.1,
  "rain_mm": 0
}
```

* `GET localhost:8000/weathers?city=Astana`

`400 Bad Request - param country is empty`

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

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

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

* [Лучшие практики по созданию REST API](https://habr.com/ru/articles/351890/)
* [Начало работы с Sinatra](https://sinatrarb.com/intro.html)
* [Rack](https://andrykonchin.github.io/ruby/2020/05/09/rack-under-the-hood.html)
* [Разбор составления структуры Sinatra приложения](https://www.toptal.com/ruby/api-with-sinatra-and-sequel-ruby-tutorial)
