Інтро
Після приблизно 4-5 років роботи з Ruby on Rails, я все ще отримую задоволення від роботи з цим фреймворком. Екосистема чудова, загальна архітектура задоволняє вимогам, а команда знає, як розвивати проект (ActionCable і API-режим в Rails 5 показують, що проект не намагається конкурувати з javascript-фреймворками).
Водночас шкодую, що застряг: я не можу написати простий веб-застосунок на Ruby без цього фреймворка. Тому я подумав, чому б не спробувати щось нове і не написати JSON-API повністю без Rails.
Вимоги
Наразі їх мінімум. Ми реалізуємо цифрову бібліотеку книг. Що нам потрібно зараз:
- Маршрутизація
- Обробка паролів
- Рендеринг JSON
Деякі речі, які я притримаю для наступних частин:
- БД
- Незалежна бізнес-логіка
- Аутентифікація
- Авторизація
- GUI
- Тести
Що буде використано
Було б цікаво обмежити себе лише STDlib
, але я не думаю, що це буде практично. Тому я буду безсовісно використовувати цей шанс, щоб випробувати кілька гемів, які привернули мою увагу:
- Grape – JSON-API фреймворк для Ruby. Він має багато схожого з Rails. Він досить чітко визначений, але не нав'язує жорсткої структури та (у цьому прикладі) буде відповідати лише за маршрутизацію та обробку паролів.
- Rack для розгортання сервера.
Цей список може бути розширений у майбутньому, але наразі він відповідає нашим скромним вимогам.
Починаємо з простого
Щоб побачити, що ми могли б зробити, якби не використовували Grape, і щоб звикнути до ітераційної розробки, ми реалізуємо найпростіший JSON-API.
application = proc do
json = { 'message' => 'Hello, world!' }
header = { 'Content-Type' => 'application/json' }
status = 200
[status, header, [json.to_s]]
end
run application
Тепер ми можемо запустити наш сервер за допомогою rackup
:
$ cd the-folder-where-the-config-file-is/
$ rackup
Puma starting in single mode...
* Version 3.10.0 (ruby 2.3.3-p222), codename: Russell's Teapot
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop
Ми можемо побачити, працює він чи ні, використовуючи Curl
:
$ curl http://localhost:9292
{"message"=>"Hello, world!"}
Що тут відбувається? Ми реалізували «Rack-інтерфейс», створивши об'єкт, який відповідає на #call (env
) і повертає масив [status, headers, body]
. «Зачекай!» – скажете ви. Де ми передаємо env
? Ми не маємо необхідності використовувати proc
. Це досить простий аспект Procs
:
Для procs, створених з використанням lambda або -> (), виникає помилка, якщо неправильна кількість параметрів передається в proc. Для procs, створених з використанням Proc.new або Kernel.proc, додаткові параметри відкидаються, а відсутні параметри мають значення nil.
Основи Grape
Тепер у нас є робочий API! Але це досить нудно. І оскільки я хотів показати Grape
, ми швидко замінимо наш Proc
чимось складнішим. Оскільки ми поки не маємо ніякого реального управління залежностями, переконайтеся, що:
$ gem install grape
А потім замініть ваш proc
на базовий API-інтерфейс grape:
# config.ru
require 'grape'
class MyApi < Grape::API
get '/' do
{message: 'hello'}
end
end
run MyApi
Ця примітивна дрібниця вже емулює Proc, який ми використовували вище. Перевіримо це, ввівши curl
рядок, що і раніше. Щоб побачити, як легко додавати параметри, реалізуємо можливість вітати когось по імені.
# config.ru
require 'grape'
class MyApi < Grape::API
get '/' do
{message: 'hello'}
end
route_param :name do
get do
{message: "Hello #{params[:name]}!"}
end
end
end
run MyApi
Ми додали параметр маршруту name
, який ми потім інтерпретувати в повідомлення. Ми можемо отримати доступ до всіх параметрів через об'єкт params
, незалежно від того, чи є вони частиною маршруту чи ні. Тепер ми перезапускаємо наш сервер і вводимо curl
:
$ curl http://localhost:9292/paul
{"message" => "Hello, paul!"}
Таким чином, ми можемо визначати маршрути та аналізувати параметри. Що далі? Нам варто подивитися, чи можемо ми відправити щось на сервер. Як щодо книги. І поки ми на ньому, ми повинні почати завантаження нашої логіки з config.ru
.
# api.rb
class BookAPI < Grape::API
format :json
helpers do
def books
@books ||= []
end
end
resource :books do
get '/' do
{books: books}
end
params do
requires :title, type: String
requires :author, type: String
end
post do
books << { author: params[:author], title: params[:title] }
end
end
end
Ми використовуватимемо config.ru
тільки для завантаження залежностей, а потім запустимо наш додаток.
# config.ru
require 'grape'
require_relative 'api'
run BookAPI
Ми перенесли API до іншого файлу та зробили кілька коригувань.
- Ми визначили
books
помічника, щоб відслідковувати всі книги. - Ми використали
resource
метод для визначення наших маршрутів в області імен/books
. - Ми додали
POST
маршрут, щоб опублікувати книгу в наших книгах. - Ми додали індекс маршруту, щоб побачити, якщо наш POST нічого не зробив.
- Ми додали
format :json
, щоб grape автоматично перетворював значення, які повертаються, в JSON.
Ви знаєте, що робити: перезапустіть сервер і отримайте curl
:
$ curl --data "title=Lord of the Rings&author=J. R. R. Tolkien" http://localhost:9292/books
[{"author":"J. R. R. Tolkien","title":"Lord of the Rings"}]
$ curl http://localhost:9292/books
{"books":[]}
Загальна картина
Це не спрацювало. Причина полягає в тому, що аналогічно контролерам Rails, екземпляри Grape :: API
не зберігаються через кілька запитів. Таким чином, @books
скидається з кожним запитом. Що має сенс, якщо є кілька людей, які запитують кілька речей. Ви не хочете, щоб вони поділилися інформацією про умови запиту. Один з способів обійти це – мати об'єкт, який існує незалежно від API для обробки даних.
# app.rb
class MyApp
def books
@books ||= []
end
end
Application = MyApp.new
Тут ми призначили екземпляр MyApp
в ролі константи, і це дає нам змогу:
- Посилатися на нього глобально.
- Заборонити збирати сміття після кожного запиту.
# config.ru
require 'grape'
require_relative 'app'
require_relative 'api'
run BookAPI
# api.rb
class BookAPI < Grape::API
format :json
helpers do
def books
Application.books
end
end
resource :books do
get '/' do
{books: books}
end
params do
requires :title, type: String
requires :author, type: String
end
post do
books << { author: params[:author], title: params[:title] }
end
end
end
Після всіх цих рядків, наш сервер нарешті працює! Ура!
$ curl --data "title=Lord of the Rings&author=J. R. R. Tolkien" http://localhost:9292/books
[{"author":"J. R. R. Tolkien","title":"Lord of the Rings"}]
$ curl http://localhost:9292/books
{"books":[{"author":"J. R. R. Tolkien","title":"Lord of the Rings"}]}
Поки цього досить. В наступній частині ми додамо БД, використовуючи sequel та rake.
Ще немає коментарів