Учебное пособие по Grape Gem: как создать REST-подобный API на Ruby

Опубликовано: 2022-03-11

Как разработчикам Ruby On Rails, нам часто приходится расширять наши приложения конечными точками API для поддержки многофункциональных интернет-клиентов с большим количеством JavaScript или собственных приложений для iPhone и Android. Также бывают случаи, когда единственной целью приложения является обслуживание приложений iPhone/Android через JSON API.

В этом руководстве я покажу, как использовать Grape — REST-подобную микроинфраструктуру API для Ruby — для создания серверной поддержки в Rails для JSON API. Grape предназначен для работы в качестве монтируемого стоечного движка, дополняющего наши веб-приложения, не мешая им.

Веб-API на Ruby с использованием Grape Gem

Пример использования

Вариант использования, на котором мы сосредоточимся в этом руководстве, — это приложение, способное захватывать и просматривать сеансы парного программирования. Само приложение будет написано для iOS на языке ObjectiveC и должно будет взаимодействовать с серверной службой для хранения и извлечения данных. В этом руководстве мы сосредоточимся на создании надежной и безопасной серверной службы, поддерживающей JSON API.

API будет поддерживать методы для:

  • Вход в систему
  • Запрос отзывов о сеансе парного программирования

ПРИМЕЧАНИЕ. В дополнение к предоставлению возможности запрашивать обзоры сеансов парного программирования, настоящий API также должен предоставлять средство для отправки обзоров парного программирования для включения в базу данных. Поскольку поддержка этого через API выходит за рамки этого руководства, мы просто предположим, что база данных была заполнена примерным набором обзоров парного программирования.

К основным техническим требованиям относятся:

  • Каждый вызов API должен возвращать действительный JSON
  • Каждый неудачный вызов API должен быть записан с адекватным контекстом и информацией для последующего воспроизведения и при необходимости отлажен.

Кроме того, поскольку наше приложение должно будет обслуживать внешних клиентов, нам нужно будет позаботиться о безопасности. С этой целью:

  • Каждый запрос должен быть ограничен небольшой группой разработчиков, которых мы отслеживаем.
  • Все запросы (кроме входа/регистрации) должны быть аутентифицированы

Разработка через тестирование и RSpec

Мы будем использовать разработку через тестирование (TDD) в качестве нашего подхода к разработке программного обеспечения, чтобы обеспечить детерминированное поведение нашего API.

В целях тестирования мы будем использовать RSpec, хорошо известную среду тестирования в сообществе RubyOnRails. Поэтому в этой статье я буду ссылаться на «спецификации», а не на «тесты».

Комплексная методология тестирования состоит из «положительных» и «отрицательных» тестов. Отрицательные спецификации будут указывать, например, как ведет себя конечная точка API, если некоторые параметры отсутствуют или неверны. Положительные спецификации охватывают случаи, когда API вызывается правильно.

Начиная

Давайте заложим основу для нашего внутреннего API. Во-первых, нам нужно создать новое приложение rails:

 rails new toptal_grape_blog

Далее мы установим RSpec, добавив rspec-rails в наш gemfile:

 group :development, :test do gem 'rspec-rails', '~> 3.2' end

Затем из нашей командной строки нам нужно запустить:

 rails generate rspec:install

Мы также можем использовать некоторое существующее программное обеспечение с открытым исходным кодом для нашей среды тестирования. Конкретно:

  • Devise — гибкое решение для аутентификации для Rails на основе Warden
  • factory_girl_rails — обеспечивает интеграцию с Rails для factory_girl, библиотеки для настройки объектов Ruby в качестве тестовых данных.

Шаг 1: Добавьте их в наш gemfile:

 ... gem 'devise' ... group :development, :test do ... gem 'factory_girl_rails', '~> 4.5' ... end

Шаг 2: Создайте модель пользователя, инициализируйте гем devise и добавьте его в модель пользователя (это позволяет использовать класс пользователя для аутентификации).

 rails g model user rails generate devise:install rails generate devise user

Шаг 3: Включите метод синтаксиса factory_girl в наш файл rails_helper.rb , чтобы использовать сокращенную версию создания пользователя в наших спецификациях:

 RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods

Шаг 4: Добавьте гем винограда в наш DSL и установите его:

 gem 'grape' bundle

Вариант использования входа пользователя и спецификация

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

 require 'rails_helper' describe '/api/login' do context 'negative tests' do context 'missing params' do context 'password' do end context 'email' do end end context 'invalid params' do context 'incorrect password' do end context 'with a non-existent login' do end end end context 'positive tests' do context 'valid params' do end end end

Если какой-либо из параметров отсутствует, клиент должен получить код состояния возврата HTTP 400 (т. е. неверный запрос) вместе с сообщением об ошибке «отсутствует электронная почта» или «отсутствует пароль».

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

Давайте создадим пользователя и хеш параметра в начале спецификации. Мы поместим этот код после блока описания:

 describe '/api/login' do let(:email) { user.email } let(:password) { user.password } let!(:user) { create :user } let(:original_params) { { email: email, password: password } } let(:params) { original_params } ...

Затем мы можем расширить наш контекст «отсутствующие параметры»/«пароль» следующим образом:

 let(:params) { original_params.except(:password) } it_behaves_like '400' it_behaves_like 'json result' it_behaves_like 'contains error msg', 'password is missing'

Но вместо того, чтобы повторять ожидания в контекстах «электронная почта» и «пароль», мы можем использовать одни и те же общие примеры в качестве ожиданий. Для этого нам нужно раскомментировать эту строку в нашем файле rails_helper.rb :

 Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

Затем нам нужно добавить 3 общих примера RSpec в spec/support/shared.rb :

 RSpec.shared_examples 'json result' do specify 'returns JSON' do api_call params expect { JSON.parse(response.body) }.not_to raise_error end end RSpec.shared_examples '400' do specify 'returns 400' do api_call params expect(response.status).to eq(400) end end RSpec.shared_examples 'contains error msg' do |msg| specify "error msg is #{msg}" do api_call params json = JSON.parse(response.body) expect(json['error_msg']).to eq(msg) end end

Эти общие примеры вызывают метод api_call , который позволяет нам определить конечную точку API только один раз в нашей спецификации (в соответствии с принципом DRY). Мы определяем этот метод следующим образом:

 describe '/api/login' do ... def api_call *params post "/api/login", *params end ...

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

 FactoryGirl.define do factory :user do password "Passw0rd" password_confirmation { |u| u.password } sequence(:email) { |n| "test#{n}@example.com" } end end

И, наконец, перед запуском наших спецификаций нам нужно запустить наши миграции:

 rake db:migrate

Имейте в виду, однако, что на этом этапе спецификации по-прежнему не работают, поскольку мы еще не реализовали конечную точку API. Это следующее.

Реализация конечной точки API входа

Для начала напишем пустой скелет для нашего API авторизации ( app/api/login.rb ):

 class Login < Grape::API format :json desc 'End-points for the Login' namespace :login do desc 'Login via email and password' params do requires :email, type: String, desc: 'email' requires :password, type: String, desc: 'password' end post do end end end

Далее мы напишем класс-агрегатор, который объединяет конечные точки API ( app/api/api.rb ):

 class API < Grape::API prefix 'api' mount Login end

Хорошо, теперь мы можем смонтировать наш API в маршрутах:

 Rails.application.routes.draw do ... mount API => '/' ... end

Теперь добавим код для проверки отсутствующих параметров. Мы можем добавить этот код в api.rb , спасая от Grape::Exceptions::ValidationErrors .

 rescue_from Grape::Exceptions::ValidationErrors do |e| rack_response({ status: e.status, error_msg: e.message, }.to_json, 400) end

В случае неверного пароля мы проверим, является ли код ответа http 401, что означает несанкционированный доступ. Давайте добавим это в контекст «неверный пароль»:

 let(:params) { original_params.merge(password: 'invalid') } it_behaves_like '401' it_behaves_like 'json result' it_behaves_like 'contains error msg', 'Bad Authentication Parameters'

Затем та же логика добавляется к контексту «с несуществующим логином».

Затем мы реализуем логику, которая обрабатывает недействительные попытки аутентификации, в наш login.rb следующим образом:

 post do user = User.find_by_email params[:email] if user.present? && user.valid_password?(params[:password]) else error_msg = 'Bad Authentication Parameters' error!({ 'error_msg' => error_msg }, 401) end end

На этом этапе все отрицательные спецификации для API входа в систему будут работать правильно, но нам все еще нужно поддерживать положительные спецификации для нашего API входа. Наша положительная спецификация ожидает, что конечная точка вернет код ответа HTTP 200 (т. е. успех) с действительным JSON и действительным токеном:

 it_behaves_like '200' it_behaves_like 'json result' specify 'returns the token as part of the response' do api_call params expect(JSON.parse(response.body)['token']).to be_present end

Давайте также добавим ожидание кода ответа 200 в spec/support/shared.rb :

 RSpec.shared_examples '200' do specify 'returns 200' do api_call params expect(response.status).to eq(200) end end

В случае успешного входа в систему мы собираемся вернуть первый действительный authentication_token вместе с электронной почтой пользователя в следующем формате:

 {'email':<the_email_of_the_user>, 'token':<the users first valid token>}

Если такого токена еще нет, то мы создадим его для текущего пользователя:

 ... if user.present? && user.valid_password?(params[:password]) token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user) status 200 else ...

Чтобы это работало, нам понадобится класс AuthenticationToken , принадлежащий пользователю. Мы создадим эту модель, а затем запустим соответствующую миграцию:

 rails g model authentication_token token user:references expires_at:datetime rake db:migrate

Нам также нужно добавить соответствующую ассоциацию в нашу пользовательскую модель:

 class User < ActiveRecord::Base has_many :authentication_tokens end

Затем мы добавим допустимую область действия в класс AuthenticationToken :

 class AuthenticationToken < ActiveRecord::Base belongs_to :user validates :token, presence: true scope :valid, -> { where{ (expires_at == nil) | (expires_at > Time.zone.now) } } end

Обратите внимание, что мы использовали синтаксис Ruby в операторе where . Это возможно благодаря нашему использованию squeel gem, который включает поддержку синтаксиса Ruby в запросах activerecord.

Для проверенного пользователя мы собираемся создать сущность, которую мы будем называть «пользователь с токеном», используя возможности драгоценного камня grape-entity .

Давайте напишем спецификацию для нашей сущности и поместим ее в файл user_with_token_entity_spec.rb :

 require 'rails_helper' describe Entities::UserWithTokenEntity do describe 'fields' do subject(:subject) { Entities::UserWithTokenEntity } specify { expect(subject).to represent(:email)} let!(:token) { create :authentication_token } specify 'presents the first available token' do json = Entities::UserWithTokenEntity.new(token.user).as_json expect(json[:token]).to be_present end end end

Далее добавим сущности в user_entity.rb :

 module Entities class UserEntity < Grape::Entity expose :email end end

И, наконец, добавьте еще один класс в user_with_token_entity.rb :

 module Entities class UserWithTokenEntity < UserEntity expose :token do |user, options| user.authentication_tokens.valid.first.token end end end

Поскольку мы не хотим, чтобы токены оставались действительными бесконечно, срок их действия истекает через один день:

 FactoryGirl.define do factory :authentication_token do token "MyString" expires_at 1.day.from_now user end end

Теперь, когда все это сделано, мы можем вернуть ожидаемый формат JSON с нашим недавно написанным UserWithTokenEntity :

 ... user = User.find_by_email params[:email] if user.present? && user.valid_password?(params[:password]) token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user) status 200 present token.user, with: Entities::UserWithTokenEntity else ...

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

Конечная точка API обзора сеанса парного программирования: начало работы

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

Наша новая конечная точка API будет подключена к /api/pair_programming_session и будет возвращать отзывы, относящиеся к проекту. Давайте начнем с написания базового скелета для этой спецификации:

 require 'rails_helper' describe '/api' do describe '/pair_programming_session' do def api_call *params get '/api/pair_programming_sessions', *params end context 'invalid params' do end context 'valid params' do end end end

Мы также напишем соответствующую пустую конечную точку API ( app/api/pair_programming_sessions.rb ):

 class PairProgrammingSessions < Grape::API format :json desc 'End-points for the PairProgrammingSessions' namespace :pair_programming_sessions do desc 'Retrieve the pairprogramming sessions' params do requires :token, type: String, desc: 'email' end get do end end end

Затем смонтируем наш новый API ( app/api/api.rb ):

 ... mount Login mount PairProgrammingSessions end

Давайте расширим спецификацию и конечную точку API в соответствии с требованиями один за другим.

Конечная точка API проверки сеанса парного программирования: проверка

Одним из наших наиболее важных нефункциональных требований безопасности было ограничение доступа к API для небольшого подмножества разработчиков, которых мы отслеживаем, поэтому давайте уточним, что:

 ... def api_call *params get '/api/pair_programming_sessions', *params end let(:token) { create :authentication_token } let(:original_params) { { token: token.token} } let(:params) { original_params } it_behaves_like 'restricted for developers' context 'invalid params' do ...

Затем мы создадим shared_example в нашем shared.rb , чтобы подтвердить, что запрос исходит от одного из наших зарегистрированных разработчиков:

 RSpec.shared_examples 'restricted for developers' do context 'without developer key' do specify 'should be an unauthorized call' do api_call params expect(response.status).to eq(401) end specify 'error code is 1001' do api_call params json = JSON.parse(response.body) expect(json['error_code']).to eq(ErrorCodes::DEVELOPER_KEY_MISSING) end end end

Нам также потребуется создать класс ErrorCodesapp/models/error_codes.rb ):

 module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end

Поскольку мы ожидаем, что наш API будет расширяться в будущем, мы собираемся реализовать authorization_helper , который можно повторно использовать во всех конечных точках API в приложении, чтобы ограничить доступ только для зарегистрированных разработчиков:

 class PairProgrammingSessions < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers }

Мы собираемся определить метод restrict_access_to_developers в модуле ApiHelpers::AuthenticationHerlper ( app/api/api_helpers/authentication_helper.rb ). Этот метод просто проверит, содержит ли ключ Authorization под заголовками действительный ApiKey . (Каждому разработчику, желающему получить доступ к API, потребуется действующий ApiKey . Он может быть предоставлен либо системным администратором, либо с помощью какого-либо автоматизированного процесса регистрации, но этот механизм выходит за рамки этой статьи.)

 module ApiHelpers module AuthenticationHelper def restrict_access_to_developers header_token = headers['Authorization'] key = ApiKey.where{ token == my{ header_token } } Rails.logger.info "API call: #{headers}\tWith params: #{params.inspect}" if ENV['DEBUG'] if key.blank? error_code = ErrorCodes::DEVELOPER_KEY_MISSING error_msg = 'please aquire a developer key' error!({ :error_msg => error_msg, :error_code => error_code }, 401) # LogAudit.new({env:env}).execute end end end end

Затем нам нужно сгенерировать модель ApiKey и запустить миграцию: rails g model api_key token rake db:migrate

После этого в нашем spec/api/pair_programming_spec.rb мы можем проверить, аутентифицирован ли пользователь:

 ... it_behaves_like 'restricted for developers' it_behaves_like 'unauthenticated' ...

Давайте также определим общий пример без unauthenticated , который можно повторно использовать во всех спецификациях ( spec/support/shared.rb ):

 RSpec.shared_examples 'unauthenticated' do context 'unauthenticated' do specify 'returns 401 without token' do api_call params.except(:token), developer_header expect(response.status).to eq(401) end specify 'returns JSON' do api_call params.except(:token), developer_header json = JSON.parse(response.body) end end end

Этот общий пример нуждается в токене в заголовке разработчика, поэтому давайте добавим его в нашу спецификацию ( spec/api/pair_programming_spec.rb ):

 ... describe '/api' do let(:api_key) { create :api_key } let(:developer_header) { {'Authorization' => api_key.token} } ...

Теперь в нашем app/api/pair_programming_session.rb аутентифицировать пользователя:

 ... class PairProgrammingSessions < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers } before { authenticate! } ...

Давайте реализуем authenticate! метод в AuthenticationHelper ( app/api/api_helpers/authentication_helper.rb ):

 ... module ApiHelpers module AuthenticationHelper TOKEN_PARAM_NAME = :token def token_value_from_request(token_param = TOKEN_PARAM_NAME) params[token_param] end def current_user token = AuthenticationToken.find_by_token(token_value_from_request) return nil unless token.present? @current_user ||= token.user end def signed_in? !!current_user end def authenticate! unless signed_in? AuditLog.create data: 'unauthenticated user access' error!({ :error_msg => "authentication_error", :error_code => ErrorCodes::BAD_AUTHENTICATION_PARAMS }, 401) end end ...

(Обратите внимание, что нам нужно добавить код ошибки BAD_AUTHENTICATION_PARAMS в наш класс ErrorCodes .)

Далее давайте уточним, что произойдет, если разработчик вызовет API с недопустимым токеном. В этом случае код возврата будет 401, сигнализирующий о «несанкционированном доступе». Результат должен быть в формате JSON, и должен быть создан объект аудита. Поэтому мы добавляем это в spec/api/pair_programming_spec.rb :

 ... context 'invalid params' do context 'incorrect token' do let(:params) { original_params.merge(token: 'invalid') } it_behaves_like '401' it_behaves_like 'json result' it_behaves_like 'auditable created' it_behaves_like 'contains error msg', 'authentication_error' it_behaves_like 'contains error code', ErrorCodes::BAD_AUTHENTICATION_PARAMS end end ...

Мы добавим общие примеры «создано с возможностью аудита», «содержит код ошибки» и «содержит сообщение об ошибке» в spec/support/shared.rb :

 ... RSpec.shared_examples 'contains error code' do |code| specify "error code is #{code}" do api_call params, developer_header json = JSON.parse(response.body) expect(json['error_code']).to eq(code) end end RSpec.shared_examples 'contains error msg' do |msg| specify "error msg is #{msg}" do api_call params, developer_header json = JSON.parse(response.body) expect(json['error_msg']).to eq(msg) end end RSpec.shared_examples 'auditable created' do specify 'creates an api call audit' do expect do api_call params, developer_header end.to change{ AuditLog.count }.by(1) end end ...

Нам также нужно создать модель audit_log:

 rails g model audit_log backtrace data user:references rake db:migrate

Конечная точка API просмотра сеанса парного программирования: возврат результатов

Для аутентифицированного и авторизованного пользователя вызов этой конечной точки API должен возвращать набор обзоров сеансов парного программирования, сгруппированных по проектам. Давайте соответствующим образом изменим наш spec/api/pair_programming_spec.rb :

 ... context 'valid params' do it_behaves_like '200' it_behaves_like 'json result' end ...

Это указывает, что запрос, отправленный с действительным api_key и действительными параметрами, возвращает код HTTP 200 (т. е. успех) и что результат возвращается в форме действительного JSON.

Мы собираемся запросить, а затем вернуть в формате JSON те сеансы парного программирования, в которых любой из участников является текущим пользователем ( app/api/pair_programming_sessions.rb ):

 ... get do sessions = PairProgrammingSession.where{(host_user == my{current_user}) | (visitor_user == my{current_user})} sessions = sessions.includes(:project, :host_user, :visitor_user, reviews: [:code_samples, :user] ) present sessions, with: Entities::PairProgrammingSessionsEntity end ...

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

  • Связь 1-ко-многим между проектами и сеансами парного программирования
  • Отношения 1 ко многим между сеансами парного программирования и обзорами
  • Отношение 1 ко многим между обзорами и примерами кода

Давайте создадим модели соответствующим образом, а затем запустим миграции:

 rails g model project name rails g model pair_programming_session project:references host_user:references visitor_user:references rails g model review pair_programming_session:references user:references comment rails g model code_sample review:references code:text rake db:migrate

Затем нам нужно изменить наши PairProgrammingSession и Review , чтобы они содержали ассоциации has_many :

 class Review < ActiveRecord::Base belongs_to :pair_programming_session belongs_to :user has_many :code_samples end class PairProgrammingSession < ActiveRecord::Base belongs_to :project belongs_to :host_user, class_name: :User belongs_to :visitor_user, class_name: 'User' has_many :reviews end

ПРИМЕЧАНИЕ. В обычных обстоятельствах я бы сгенерировал эти классы, сначала написав для них спецификации, но, поскольку это выходит за рамки этой статьи, я пропущу этот шаг.

Теперь нам нужно написать те классы, которые будут преобразовывать наши модели в их представления JSON (называемые виноградными сущностями в терминологии винограда). Для простоты мы будем использовать сопоставление 1-к-1 между моделями и виноградными сущностями.

Начнем с предоставления поля code из CodeSampleEntityapi/entities/code_sample_entity.rb ):

 module Entities class CodeSampleEntity < Grape::Entity expose :code end end

Затем мы выставляем user и связанные с ним code_samples , повторно используя уже определенные UserEntity и CodeSampleEntity :

 module Entities class ReviewEntity < Grape::Entity expose :user, using: UserEntity expose :code_samples, using: CodeSampleEntity end end

Мы также выставляем поле name из ProjectEntity :

 module Entities class ProjectEntity < Grape::Entity expose :name end end

Наконец, мы собираем объект в новый PairProgrammingSessionsEntity , где мы предоставляем project , host_user , visitor_user и reviews :

 module Entities class PairProgrammingSessionsEntity < Grape::Entity expose :project, using: ProjectEntity expose :host_user, using: UserEntity expose :visitor_user, using: UserEntity expose :reviews, using: ReviewEntity end end

И при этом наш API полностью реализован!

Генерация тестовых данных

В целях тестирования мы создадим некоторые образцы данных в db/seeds.rb . Этот файл должен содержать все записи, необходимые для заполнения базы данных значениями по умолчанию. Затем данные могут быть загружены с помощью rake db:seed (или созданы с помощью db при вызове db:setup ). Вот пример того, что это может включать:

 user_1 = User.create email: '[email protected]', password: 'password', password_confirmation: 'password' user_2 = User.create email: '[email protected]', password: 'password', password_confirmation: 'password' user_3 = User.create email: '[email protected]', password: 'password', password_confirmation: 'password' ApiKey.create token: '12345654321' project_1 = Project.create name: 'Time Sheets' project_2 = Project.create name: 'Toptal Blog' project_3 = Project.create name: 'Hobby Project' session_1 = PairProgrammingSession.create project: project_1, host_user: user_1, visitor_user: user_2 session_2 = PairProgrammingSession.create project: project_2, host_user: user_1, visitor_user: user_3 session_3 = PairProgrammingSession.create project: project_3, host_user: user_2, visitor_user: user_3 review_1 = session_1.reviews.create user: user_1, comment: 'Please DRY a bit your code' review_2 = session_1.reviews.create user: user_1, comment: 'Please DRY a bit your specs' review_3 = session_2.reviews.create user: user_1, comment: 'Please DRY your view templates' review_4 = session_2.reviews.create user: user_1, comment: 'Please clean your N+1 queries' review_1.code_samples.create code: 'Lorem Ipsum' review_1.code_samples.create code: 'Do not abuse the single responsibility principle' review_2.code_samples.create code: 'Use some shared examples' review_2.code_samples.create code: 'Use at the beginning of specs'

Теперь наше приложение готово к использованию, и мы можем запустить наш сервер rails.

Тестирование API

Мы будем использовать Swagger для ручного тестирования нашего API в браузере. Однако нам необходимо выполнить несколько шагов по настройке, чтобы мы могли использовать Swagger.

Во-первых, нам нужно добавить пару драгоценных камней в наш gemfile:

 ... gem 'grape-swagger' gem 'grape-swagger-ui' ...

Затем мы запускаем bundle для установки этих драгоценных камней.

Нам также нужно добавить их к активам в наш конвейер активов (в config/initializers/assets.rb ):

 Rails.application.config.assets.precompile += %w( swagger_ui.js ) Rails.application.config.assets.precompile += %w( swagger_ui.css )

Наконец, в app/api/api.rb нам нужно смонтировать генератор swagger:

 ... add_swagger_documentation end ...

Теперь мы можем воспользоваться удобным пользовательским интерфейсом Swagger для изучения нашего API, просто перейдя по http://localhost:3000/api/swagger .

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

Одна оставшаяся деталь, прежде чем мы продолжим: поскольку мы ограничили использование разработчиков API действительным api_key , мы не сможем получить доступ к конечной точке API напрямую из браузера, поскольку серверу потребуется действительный api_key в заголовке HTTP. Мы можем выполнить это в целях тестирования в Google Chrome, используя плагин Modify Headers for Google Chrome. Этот плагин позволит нам отредактировать HTTP-заголовок и добавить действительный api_key (мы будем использовать фиктивный ключ 12345654321 api_key который мы включили в начальный файл нашей базы данных).

Хорошо, теперь мы готовы к тесту!

Чтобы вызвать конечную точку API pair_programming_sessions , нам сначала нужно войти в систему. Мы просто используем комбинации электронной почты и пароля из нашего начального файла базы данных и отправим их через Swagger в конечную точку API входа, как показано ниже.

Как вы можете видеть выше, токен, принадлежащий этому пользователю, возвращается, что указывает на то, что API входа в систему работает должным образом, как и предполагалось. Теперь мы можем использовать этот токен для успешного выполнения операции GET /api/pair_programming_sessions.json .

Как показано, результат возвращается в виде правильно отформатированного иерархического объекта JSON. Обратите внимание, что структура JSON отражает две вложенные ассоциации 1-ко-многим, поскольку проект имеет несколько обзоров, а обзор содержит несколько примеров кода. Если бы мы не возвращали структуру таким образом, вызывающей стороне нашего API пришлось бы отдельно запрашивать обзоры для каждого проекта, что потребовало бы отправки N запросов в конечную точку нашего API. Таким образом, с помощью этой структуры мы решаем проблему производительности запросов N+1.

Заворачивать

Как показано здесь, исчерпывающие спецификации для вашего API помогают гарантировать, что реализованный API правильно и адекватно соответствует предполагаемым (и непреднамеренным!) вариантам использования.

Хотя пример API, представленный в этом руководстве, является довольно простым, подход и методы, которые мы продемонстрировали, могут служить основой для более сложных API произвольной сложности с использованием драгоценного камня Grape. Мы надеемся, что это руководство продемонстрировало, что Grape — полезный и гибкий драгоценный камень, который может помочь упростить реализацию JSON API в ваших приложениях Rails. Наслаждаться!