Tutorial Grape Gem: Como construir uma API REST-Like em Ruby

Publicados: 2022-03-11

Como desenvolvedores Ruby On Rails, muitas vezes precisamos estender nossos aplicativos com terminais de API para suportar clientes Rich Internet pesados ​​em JavaScript ou aplicativos nativos para iPhone e Android. Há também alguns casos em que o único propósito do aplicativo é servir os aplicativos para iPhone/Android por meio de uma API JSON.

Neste tutorial, demonstro como usar Grape – um micro-framework de API do tipo REST para Ruby – para construir suporte de backend em Rails para uma API JSON. O Grape foi projetado para funcionar como um mecanismo de rack montável para complementar nossos aplicativos da Web, sem interferir neles.

API da Web em Ruby usando Grape Gem

Caso de uso

O caso de uso que focaremos neste tutorial é um aplicativo capaz de capturar e revisar sessões de programação em par. O aplicativo em si será escrito para iOS em ObjectiveC e precisará se comunicar com um serviço de back-end para armazenar e recuperar os dados. Nosso foco neste tutorial é a criação de um serviço de back-end robusto e seguro que oferece suporte a uma API JSON.

A API oferecerá suporte a métodos para:

  • Fazendo login no sistema
  • Consultando revisões de sessão de programação em par

NOTA: Além de fornecer a capacidade de consultar revisões de sessão de programação em par, a API real também precisaria fornecer um recurso para enviar revisões de programação em par para inclusão no banco de dados. Como o suporte a isso por meio da API está além do escopo deste tutorial, simplesmente assumiremos que o banco de dados foi preenchido com um conjunto de amostras de revisões de programação em par.

Os principais requisitos técnicos incluem:

  • Cada chamada de API deve retornar um JSON válido
  • Cada chamada de API com falha deve ser registrada com contexto e informações adequados para posteriormente ser reproduzível e depurada, se necessário

Além disso, como nosso aplicativo precisará atender clientes externos, precisaremos nos preocupar com a segurança. Para esse fim:

  • Cada solicitação deve ser restrita a um pequeno subconjunto de desenvolvedores que acompanhamos
  • Todas as solicitações (exceto login/inscrição) precisam ser autenticadas

Desenvolvimento Orientado a Testes e RSpec

Usaremos o Test Driven Development (TDD) como nossa abordagem de desenvolvimento de software para ajudar a garantir o comportamento determinístico de nossa API.

Para fins de teste, usaremos o RSpec, um framework de teste bem conhecido na comunidade RubyOnRails. Por isso, vou me referir neste artigo a "especificações" em vez de "testes".

Uma metodologia de teste abrangente consiste em testes “positivos” e “negativos”. As especificações negativas especificarão, por exemplo, como o endpoint da API se comporta se alguns parâmetros estiverem ausentes ou incorretos. As especificações positivas abrangem casos em que a API é invocada corretamente.

Começando

Vamos lançar as bases para nossa API de back-end. Primeiro, precisamos criar um novo aplicativo Rails:

 rails new toptal_grape_blog

Em seguida, instalaremos o RSpec adicionando rspec-rails em nosso gemfile:

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

Então, da nossa linha de comando, precisamos executar:

 rails generate rspec:install

Também podemos aproveitar alguns softwares de código aberto existentes para nossa estrutura de teste. Especificamente:

  • Devise - uma solução de autenticação flexível para Rails baseada em Warden
  • factory_girl_rails - fornece integração Rails para factory_girl, uma biblioteca para configurar objetos Ruby como dados de teste

Etapa 1: adicione-os ao nosso gemfile:

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

Etapa 2: gerar um modelo de usuário, inicializar a gem devise e adicioná-la ao modelo de usuário (isso permite que a classe de usuário seja usada para autenticação).

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

Etapa 3: inclua o método de sintaxe factory_girl em nosso arquivo rails_helper.rb para usar a versão abreviada da criação do usuário em nossas especificações:

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

Etapa 4: adicione a gema de uva ao nosso DSL e instale-a:

 gem 'grape' bundle

Caso de uso e especificações de login do usuário

Nosso back-end precisará oferecer suporte a um recurso básico de login. Vamos criar o esqueleto para nosso login_spec , assumindo que uma solicitação de login válida consiste em um endereço de e-mail registrado e um par de senhas:

 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

Se um dos parâmetros estiver ausente, o cliente deverá receber um código de status de retorno HTTP de 400 (ou seja, Solicitação inválida), juntamente com uma mensagem de erro de 'falta de e-mail' ou 'falta de senha'.

Para nosso teste, criaremos um usuário válido e definiremos o e-mail e a senha do usuário como parâmetros originais para este conjunto de testes. Em seguida, personalizaremos esse hash de parâmetro para cada especificação específica, omitindo a senha/e-mail ou substituindo-a.

Vamos criar o usuário e o hash do parâmetro no início da especificação. Vamos colocar este código após o bloco describe:

 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 } ...

Podemos então estender nosso contexto de 'parâmetros ausentes'/'senha' da seguinte maneira:

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

Mas, em vez de repetir as expectativas nos contextos de 'e-mail' e 'senha', podemos usar os mesmos exemplos compartilhados como expectativas. Para isso, precisamos descomentar esta linha em nosso arquivo rails_helper.rb :

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

Em seguida, precisamos adicionar os 3 exemplos compartilhados RSpec em 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

Esses exemplos compartilhados estão chamando o método api_call que nos permite definir o endpoint da API apenas uma vez em nossa especificação (de acordo com o princípio DRY). Definimos esse método da seguinte forma:

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

Também precisaremos personalizar a fábrica para nosso usuário:

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

E, finalmente, antes de executar nossas especificações, precisamos executar nossas migrações:

 rake db:migrate

No entanto, lembre-se de que as especificações ainda falharão neste momento, pois ainda não implementamos nosso endpoint de API. Isso é o próximo.

Implementando o endpoint da API de login

Para começar, escreveremos um esqueleto vazio para nossa API de login ( 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

Em seguida, escreveremos uma classe agregadora que agrega os endpoints da API ( app/api/api.rb ):

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

OK, agora podemos montar nossa API nas rotas:

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

Agora vamos adicionar o código para verificar os parâmetros ausentes. Podemos adicionar esse código ao api.rb resgatando de Grape::Exceptions::ValidationErrors .

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

Para a senha inválida, verificaremos se o código de resposta http é 401, o que significa acesso não autorizado. Vamos adicionar isso ao contexto 'senha incorreta':

 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'

A mesma lógica é então adicionada ao contexto 'com um login inexistente' também.

Em seguida, implementamos a lógica que trata as tentativas de autenticação inválidas em nosso login.rb da seguinte forma:

 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

Neste ponto, todas as especificações negativas para a API de login se comportarão corretamente, mas ainda precisamos oferecer suporte às especificações positivas para nossa API de login. Nossa especificação positiva espera que o endpoint retorne um código de resposta HTTP de 200 (ou seja, sucesso) com JSON válido e um token válido:

 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

Vamos também adicionar a expectativa para o código de resposta 200 em spec/support/shared.rb :

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

Em caso de login bem-sucedido, retornaremos o primeiro authentication_token válido junto com o email do usuário neste formato:

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

Se ainda não houver tal token, criaremos um para o usuário atual:

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

Para que isso funcione, precisaremos de uma classe AuthenticationToken que pertença ao usuário. Vamos gerar este modelo e, em seguida, executar a migração correspondente:

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

Também precisamos adicionar a associação correspondente ao nosso modelo de usuário:

 class User < ActiveRecord::Base has_many :authentication_tokens end

Em seguida, adicionaremos o escopo válido à classe AuthenticationToken :

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

Observe que usamos a sintaxe Ruby na instrução where . Isso é habilitado pelo nosso uso da gem squeel que habilita o suporte à sintaxe Ruby em consultas activerecord.

Para um usuário validado, vamos criar uma entidade que chamaremos de “usuário com entidade de token”, aproveitando os recursos da gem grape-entity .

Vamos escrever a especificação para nossa entidade e colocá-la no arquivo 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

Em seguida, vamos adicionar as entidades a user_entity.rb :

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

E, finalmente, adicione outra classe a user_with_token_entity.rb :

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

Como não queremos que os tokens permaneçam válidos indefinidamente, eles expiram após um dia:

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

Com tudo isso feito, agora podemos retornar o formato JSON esperado com nosso UserWithTokenEntity recém-escrito:

 ... 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 ...

Frio. Agora, todas as nossas especificações estão sendo aprovadas e os requisitos funcionais do endpoint da API de login básico são suportados.

Ponto de extremidade da API de revisão da sessão de programação em pares: introdução

Nosso back-end precisará permitir que desenvolvedores autorizados que fizeram login consultem revisões de sessão de programação de pares.

Nosso novo endpoint de API será montado em /api/pair_programming_session e retornará as revisões pertencentes a um projeto. Vamos começar escrevendo um esqueleto básico para esta especificação:

 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

Também escreveremos um endpoint de API vazio correspondente ( 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

Então vamos montar nossa nova api ( app/api/api.rb ):

 ... mount Login mount PairProgrammingSessions end

Vamos expandir a especificação e o endpoint da API em relação aos requisitos um por um.

Ponto de extremidade da API de revisão da sessão de programação em pares: validação

Um dos nossos requisitos de segurança não funcionais mais importantes era restringir o acesso à API a um pequeno subconjunto de desenvolvedores que rastreamos, então vamos especificar que:

 ... 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 ...

Em seguida, criaremos um shared_example em nosso shared.rb para confirmar que a solicitação está vindo de um de nossos desenvolvedores registrados:

 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

Também precisaremos criar uma classe ErrorCodes (em app/models/error_codes.rb ):

 module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end

Como esperamos que nossa API se expanda no futuro, vamos implementar um authorization_helper que pode ser reutilizado em todos os endpoints da API no aplicativo para restringir o acesso apenas a desenvolvedores registrados:

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

Vamos definir o método restrict_access_to_developers no módulo ApiHelpers::AuthenticationHerlper ( app/api/api_helpers/authentication_helper.rb ). Este método simplesmente verificará se a Authorization da chave nos cabeçalhos contém uma ApiKey válida. (Todo desenvolvedor que deseja acessar a API precisará de um ApiKey válido. Isso pode ser fornecido por um administrador do sistema ou por meio de algum processo de registro automatizado, mas esse mecanismo está além do escopo deste artigo.)

 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

Em seguida, precisamos gerar o modelo ApiKey e executar as migrações: rails g model api_key token rake db:migrate

Feito isso, em nosso spec/api/pair_programming_spec.rb podemos verificar se o usuário está autenticado:

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

Vamos também definir um exemplo compartilhado unauthenticated que pode ser reutilizado em todas as especificações ( 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

Este exemplo compartilhado precisa do token no cabeçalho do desenvolvedor, então vamos adicioná-lo à nossa especificação ( spec/api/pair_programming_spec.rb ):

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

Agora, em nosso app/api/pair_programming_session.rb , vamos tentar autenticar o usuário:

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

Vamos implementar a authenticate! método no 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 ...

(Observe que precisamos adicionar o código de erro BAD_AUTHENTICATION_PARAMS à nossa classe ErrorCodes .)

Em seguida, vamos especificar o que acontece se o desenvolvedor chamar a API com um token inválido. Nesse caso o código de retorno será 401 sinalizando um 'acesso não autorizado'. O resultado deve ser JSON e um auditável deve ser criado. Então, adicionamos isso a 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 ...

Adicionaremos os exemplos compartilhados “auditable created”, “contém código de erro” e “contém msg de erro” a 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 ...

Também precisamos criar um modelo audit_log:

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

Ponto de extremidade da API de revisão de sessão de programação em pares: resultados de retorno

Para um usuário autenticado e autorizado, uma chamada para esse endpoint da API deve retornar um conjunto de revisões de sessão de programação em pares agrupadas por projeto. Vamos modificar nosso spec/api/pair_programming_spec.rb acordo:

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

Isso especifica que uma solicitação enviada com api_key e parâmetros válidos retorna um código HTTP de 200 (ou seja, sucesso) e que o resultado é retornado na forma de JSON válido.

Vamos consultar e depois retornar em formato JSON aquelas sessões de programação em par onde qualquer um dos participantes é o usuário atual ( 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 ...

As sessões de programação em par são modeladas da seguinte forma no banco de dados:

  • Relação 1-para-muitos entre projetos e sessões de programação em pares
  • Relação de 1 para muitos entre sessões de programação em pares e revisões
  • Relação de 1 para muitos entre revisões e amostras de código

Vamos gerar os modelos de acordo e depois executar as migrações:

 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

Então precisamos modificar nossas classes PairProgrammingSession e Review para conter as associações 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

NOTA: Em circunstâncias normais, eu geraria essas classes escrevendo especificações para elas primeiro, mas como isso está além do escopo deste artigo, pularei essa etapa.

Agora precisamos escrever as classes que vão transformar nossos modelos em suas representações JSON (referidas como entidades de uva na terminologia de uva). Para simplificar, usaremos o mapeamento de 1 para 1 entre os modelos e as entidades de uva.

Começamos expondo o campo de code do CodeSampleEntity (em api/entities/code_sample_entity.rb ):

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

Em seguida, expomos o user e os code_samples associados, reutilizando o UserEntity e o CodeSampleEntity já definidos:

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

Também expomos o campo name do ProjectEntity :

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

Por fim, montamos a entidade em uma nova PairProgrammingSessionsEntity onde expomos o project , o host_user , o visitor_user e os 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

E com isso, nossa API está totalmente implementada!

Gerando dados de teste

Para fins de teste, criaremos alguns dados de exemplo em db/seeds.rb . Este arquivo deve conter toda a criação de registro necessária para propagar o banco de dados com seus valores padrão. Os dados podem então ser carregados com rake db:seed (ou criados com o db quando db:setup é invocado). Aqui está um exemplo do que isso pode incluir:

 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'

Agora nosso aplicativo está pronto para uso e podemos iniciar nosso servidor Rails.

Testando a API

Usaremos o Swagger para fazer alguns testes manuais de nossa API baseados em navegador. Algumas etapas de configuração são necessárias para que possamos usar o Swagger.

Primeiro, precisamos adicionar algumas gems ao nosso gemfile:

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

Em seguida, executamos o bundle para instalar essas gems.

Também precisamos adicioná-los aos ativos em nosso pipeline de ativos (em config/initializers/assets.rb ):

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

Finalmente, em app/api/api.rb precisamos montar o gerador de swagger:

 ... add_swagger_documentation end ...

Agora podemos aproveitar a boa interface de usuário do Swagger para explorar nossa API simplesmente acessando http://localhost:3000/api/swagger .

O Swagger apresenta nossos endpoints de API de uma maneira bem explorável. Se clicarmos em um endpoint, o Swagger lista suas operações. Se clicarmos em uma operação, o Swagger mostra seus parâmetros obrigatórios e opcionais e seus tipos de dados.

Um detalhe restante antes de prosseguirmos: como restringimos o uso dos desenvolvedores de API com uma api_key válida, não poderemos acessar o endpoint da API diretamente do navegador porque o servidor exigirá uma api_key válida no cabeçalho HTTP. Podemos fazer isso para fins de teste no Google Chrome usando o plug-in Modify Headers for Google Chrome. Este plugin nos permitirá editar o cabeçalho HTTP e adicionar uma api_key válida (vamos usar a api_key fictícia de 12345654321 que incluímos em nosso arquivo seed do banco de dados).

OK, agora estamos prontos para testar!

Para chamar o endpoint da API pair_programming_sessions , primeiro precisamos fazer login. Usaremos apenas as combinações de e-mail e senha do nosso arquivo seed do banco de dados e o enviaremos via Swagger para o endpoint da API de login, conforme mostrado abaixo.

Como você pode ver acima, o token pertencente a esse usuário é retornado, indicando que a API de login está funcionando corretamente conforme o esperado. Agora podemos usar esse token para executar com êxito a operação GET /api/pair_programming_sessions.json .

Conforme mostrado, o resultado é retornado como um objeto JSON hierárquico formatado corretamente. Observe que a estrutura JSON reflete duas associações de 1 para muitos aninhadas, pois o projeto tem várias revisões e uma revisão tem várias amostras de código. Se não retornarmos a estrutura dessa maneira, o chamador de nossa API precisaria solicitar separadamente as revisões para cada projeto, o que exigiria o envio de N consultas ao nosso endpoint de API. Com essa estrutura, resolvemos, portanto, o problema de desempenho da consulta N+1.

Embrulhar

Conforme mostrado aqui, especificações abrangentes para sua API ajudam a garantir que a API implementada atenda de forma adequada e adequada aos casos de uso pretendidos (e não intencionais!).

Embora a API de exemplo apresentada neste tutorial seja bastante básica, a abordagem e as técnicas que demonstramos podem servir como base para APIs mais sofisticadas de complexidade arbitrária usando a gem Grape. Esperamos que este tutorial tenha demonstrado que o Grape é uma gem útil e flexível que pode ajudar a facilitar a implementação de uma API JSON em seus aplicativos Rails. Aproveitar!