Grape Gem Tutorial: come creare un'API simile a REST in Ruby
Pubblicato: 2022-03-11In qualità di sviluppatori di Ruby On Rails, abbiamo spesso bisogno di estendere le nostre applicazioni con endpoint API per supportare client Rich Internet ricchi di JavaScript o app native per iPhone e Android. Ci sono anche alcuni casi in cui l'unico scopo dell'applicazione è quello di servire le app per iPhone/Android tramite un'API JSON.
In questo tutorial, mostro come utilizzare Grape, un micro-framework API simile a REST per Ruby, per creare supporto back-end in Rails per un'API JSON. Grape è progettato per funzionare come un motore rack montabile per completare le nostre applicazioni Web, senza interferire con esse.
Caso d'uso
Il caso d'uso su cui ci concentreremo per questo tutorial è un'applicazione in grado di acquisire e rivedere sessioni di programmazione di coppia. L'applicazione stessa sarà scritta per iOS in ObjectiveC e dovrà comunicare con un servizio di back-end per l'archiviazione e il recupero dei dati. Il nostro obiettivo in questo tutorial è la creazione di un servizio di back-end robusto e sicuro che supporti un'API JSON.
L'API supporterà metodi per:
- Accesso al sistema
- Interrogazione delle revisioni della sessione di programmazione di coppia
NOTA: oltre a fornire la possibilità di interrogare le revisioni della sessione di programmazione di coppia, l'API reale dovrebbe anche fornire una struttura per inviare le revisioni di programmazione di coppia da includere nel database. Dal momento che il supporto tramite l'API va oltre lo scopo di questo tutorial, assumeremo semplicemente che il database sia stato popolato con un set di esempio di revisioni della programmazione di coppia.
I requisiti tecnici chiave includono:
- Ogni chiamata API deve restituire un JSON valido
- Ogni chiamata API non riuscita deve essere registrata con contesto e informazioni adeguati per essere successivamente riproducibile e, se necessario, sottoposta a debug
Inoltre, poiché la nostra applicazione dovrà servire clienti esterni, dovremo preoccuparci della sicurezza. A tal fine:
- Ogni richiesta dovrebbe essere limitata a un piccolo sottoinsieme di sviluppatori che monitoriamo
- Tutte le richieste (diverse da login/registrazione) devono essere autenticate
Sviluppo basato su test e RSpec
Utilizzeremo Test Driven Development (TDD) come approccio allo sviluppo del software per garantire il comportamento deterministico della nostra API.
A scopo di test utilizzeremo RSpec, un noto framework di test nella community di RubyOnRails. Mi riferirò quindi in questo articolo alle "specifiche" piuttosto che ai "test".
Una metodologia di test completa consiste in test sia "positivi" che "negativi". Le specifiche negative specificheranno, ad esempio, come si comporta l'endpoint API se alcuni parametri sono mancanti o non corretti. Le specifiche positive coprono i casi in cui l'API viene richiamata correttamente.
Iniziare
Gettiamo le basi per la nostra API back-end. Innanzitutto, dobbiamo creare una nuova applicazione per binari:
rails new toptal_grape_blog
Successivamente, installeremo RSpec aggiungendo rspec-rails
nel nostro gemfile:
group :development, :test do gem 'rspec-rails', '~> 3.2' end
Quindi dalla nostra riga di comando dobbiamo eseguire:
rails generate rspec:install
Possiamo anche sfruttare alcuni software open source esistenti per il nostro framework di test. Nello specifico:
- Devise : una soluzione di autenticazione flessibile per Rails basata su Warden
- factory_girl_rails - fornisce l'integrazione di Rails per factory_girl, una libreria per impostare oggetti Ruby come dati di test
Passaggio 1: aggiungi questi nel nostro gemfile:
... gem 'devise' ... group :development, :test do ... gem 'factory_girl_rails', '~> 4.5' ... end
Passaggio 2: genera un modello utente, inizializza la devise
del dispositivo e aggiungilo al modello utente (questo consente di utilizzare la classe utente per l'autenticazione).
rails g model user rails generate devise:install rails generate devise user
Passaggio 3: includi il metodo della sintassi factory_girl
nel nostro file rails_helper.rb
per utilizzare la versione abbreviata della creazione dell'utente nelle nostre specifiche:
RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods
Passaggio 4: aggiungi la gemma dell'uva alla nostra DSL e installala:
gem 'grape' bundle
Accesso utente Caso d'uso e spec
Il nostro backend dovrà supportare una funzionalità di accesso di base. Creiamo lo scheletro per il nostro login_spec
, supponendo che una richiesta di accesso valida sia composta da un indirizzo email registrato e una coppia di password:
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 uno dei parametri è mancante, il client dovrebbe ricevere un codice di stato di ritorno HTTP di 400 (ad es. Richiesta non valida), insieme a un messaggio di errore "email mancante" o "password mancante".
Per il nostro test, creeremo un utente valido e imposteremo l'e-mail e la password dell'utente come parametri originali per questa suite di test. Quindi personalizzeremo questo parametro hash per ogni specifica specifica omettendo la password/e-mail o sovrascrivendola.
Creiamo l'utente e l'hash del parametro all'inizio della specifica. Metteremo questo codice dopo il blocco di descrizione:
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 } ...
Possiamo quindi estendere il nostro contesto "parametri mancanti"/"password" come segue:
let(:params) { original_params.except(:password) } it_behaves_like '400' it_behaves_like 'json result' it_behaves_like 'contains error msg', 'password is missing'
Ma invece di ripetere le aspettative nei contesti "email" e "password", possiamo utilizzare gli stessi esempi condivisi delle aspettative. Per questo, dobbiamo decommentare questa riga nel nostro file rails_helper.rb
:
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
Dobbiamo quindi aggiungere i 3 esempi condivisi RSpec in 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
Questi esempi condivisi chiamano il metodo api_call
che ci consente di definire l'endpoint API solo una volta nelle nostre specifiche (in linea con il principio DRY). Definiamo questo metodo come segue:
describe '/api/login' do ... def api_call *params post "/api/login", *params end ...
Dovremo anche personalizzare la fabbrica per il nostro utente:
FactoryGirl.define do factory :user do password "Passw0rd" password_confirmation { |u| u.password } sequence(:email) { |n| "test#{n}@example.com" } end end
E infine, prima di eseguire le nostre specifiche, dobbiamo eseguire le nostre migrazioni:
rake db:migrate
Tieni presente, tuttavia, che le specifiche continueranno a fallire a questo punto, poiché non abbiamo ancora implementato il nostro endpoint API. Questo è il prossimo.
Implementazione dell'endpoint API di accesso
Per cominciare, scriveremo uno scheletro vuoto per la nostra API di accesso ( 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
Successivamente, scriveremo una classe aggregatore che aggrega gli endpoint API ( app/api/api.rb
):
class API < Grape::API prefix 'api' mount Login end
OK, ora possiamo montare la nostra API nei percorsi:
Rails.application.routes.draw do ... mount API => '/' ... end
Ora aggiungiamo il codice per verificare i parametri mancanti. Possiamo aggiungere quel codice a api.rb
salvando da Grape::Exceptions::ValidationErrors
.
rescue_from Grape::Exceptions::ValidationErrors do |e| rack_response({ status: e.status, error_msg: e.message, }.to_json, 400) end
Per la password non valida, verificheremo se il codice di risposta http è 401, il che significa accesso non autorizzato. Aggiungiamo questo al contesto "password errata":
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'
La stessa logica viene quindi aggiunta anche al contesto "con accesso inesistente".
Quindi implementiamo la logica che gestisce i tentativi di autenticazione non validi nel nostro login.rb
come segue:
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
A questo punto tutte le specifiche negative per l'API di accesso si comporteranno correttamente, ma dobbiamo comunque supportare le specifiche positive per la nostra API di accesso. La nostra specifica positiva prevede che l'endpoint restituisca un codice di risposta HTTP di 200 (ovvero successo) con JSON valido e un token valido:
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
Aggiungiamo anche l'aspettativa per il codice di risposta 200 a spec/support/shared.rb
:
RSpec.shared_examples '200' do specify 'returns 200' do api_call params expect(response.status).to eq(200) end end
In caso di accesso riuscito, restituiremo il primo token_autenticazione valido insieme all'e-mail dell'utente in questo formato:
{'email':<the_email_of_the_user>, 'token':<the users first valid token>}
Se non esiste ancora un token del genere, ne creeremo uno per l'utente corrente:
... if user.present? && user.valid_password?(params[:password]) token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user) status 200 else ...
Affinché ciò funzioni, avremo bisogno di una classe AuthenticationToken
che appartiene all'utente. Genereremo questo modello, quindi eseguiremo la migrazione corrispondente:
rails g model authentication_token token user:references expires_at:datetime rake db:migrate
Dobbiamo anche aggiungere l'associazione corrispondente al nostro modello utente:
class User < ActiveRecord::Base has_many :authentication_tokens end
Quindi aggiungeremo l'ambito valido alla 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
Nota che abbiamo usato la sintassi di Ruby nell'istruzione where
. Ciò è reso possibile dall'uso della gemma squeel
che abilita il supporto per la sintassi Ruby nelle query activerecord.
Per un utente convalidato, creeremo un'entità che chiameremo "utente con entità token", sfruttando le caratteristiche della gemma grape-entity
.
Scriviamo la specifica per la nostra entità e la mettiamo nel file 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
Quindi aggiungiamo le entità a user_entity.rb
:
module Entities class UserEntity < Grape::Entity expose :email end end
E infine, aggiungi un'altra 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
Poiché non vogliamo che i token rimangano validi indefinitamente, li facciamo scadere dopo un giorno:
FactoryGirl.define do factory :authentication_token do token "MyString" expires_at 1.day.from_now user end end
Fatto tutto ciò, ora possiamo restituire il formato JSON previsto con il nostro UserWithTokenEntity
appena scritto:
... 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 ...
Freddo. Ora tutte le nostre specifiche stanno superando e i requisiti funzionali dell'endpoint API di accesso di base sono supportati.
Abbinare l'endpoint API di revisione della sessione di programmazione: per iniziare
Il nostro back-end dovrà consentire agli sviluppatori autorizzati che hanno effettuato l'accesso di interrogare le revisioni delle sessioni di programmazione di coppia.
Il nostro nuovo endpoint API verrà montato su /api/pair_programming_session
e restituirà le recensioni appartenenti a un progetto. Iniziamo scrivendo uno scheletro di base per questa specifica:
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
Scriveremo anche un corrispondente endpoint API vuoto ( 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
Quindi montiamo la nostra nuova API ( app/api/api.rb
):
... mount Login mount PairProgrammingSessions end
Espandiamo le specifiche e l'endpoint API rispetto ai requisiti uno per uno.
Abbinare l'endpoint API di revisione della sessione di programmazione: convalida
Uno dei nostri requisiti di sicurezza non funzionali più importanti era limitare l'accesso alle API a un piccolo sottoinsieme di sviluppatori che monitoriamo, quindi specifichiamo che:

... 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 ...
Quindi creeremo un shared_example
nel nostro shared.rb
per confermare che la richiesta proviene da uno dei nostri sviluppatori registrati:
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
Dovremo anche creare una classe ErrorCodes
(in app/models/error_codes.rb
):
module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end
Poiché ci aspettiamo che la nostra API si espanda in futuro, implementeremo authorization_helper
che può essere riutilizzato su tutti gli endpoint API nell'applicazione per limitare l'accesso solo agli sviluppatori registrati:
class PairProgrammingSessions < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers }
Definiremo il metodo restrict_access_to_developers
nel modulo ApiHelpers::AuthenticationHerlper
( app/api/api_helpers/authentication_helper.rb
). Questo metodo verificherà semplicemente se la chiave Authorization
sotto le intestazioni contiene un ApiKey
valido. (Ogni sviluppatore che desidera accedere all'API richiederà una ApiKey
valida. Potrebbe essere fornita da un amministratore di sistema o tramite un processo di registrazione automatizzato, ma questo meccanismo esula dallo scopo di questo articolo.)
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
Dobbiamo quindi generare il modello ApiKey ed eseguire le migrazioni: rails g model api_key token rake db:migrate
Fatto ciò, nel nostro spec/api/pair_programming_spec.rb
possiamo verificare se l'utente è autenticato:
... it_behaves_like 'restricted for developers' it_behaves_like 'unauthenticated' ...
Definiamo anche un esempio condiviso non unauthenticated
che può essere riutilizzato in tutte le specifiche ( 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
Questo esempio condiviso richiede il token nell'intestazione dello sviluppatore, quindi aggiungiamolo alle nostre specifiche ( spec/api/pair_programming_spec.rb
):
... describe '/api' do let(:api_key) { create :api_key } let(:developer_header) { {'Authorization' => api_key.token} } ...
Ora, nella nostra app/api/pair_programming_session.rb
, proviamo ad autenticare l'utente:
... class PairProgrammingSessions < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers } before { authenticate! } ...
Implementiamo l' authenticate!
metodo in 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 ...
(Nota che dobbiamo aggiungere il codice di errore BAD_AUTHENTICATION_PARAMS
alla nostra classe ErrorCodes
.)
Quindi, specifichiamo cosa succede se lo sviluppatore chiama l'API con un token non valido. In tal caso il codice di ritorno sarà 401 segnalando un 'accesso non autorizzato'. Il risultato dovrebbe essere JSON e dovrebbe essere creato un verificabile. Quindi lo aggiungiamo 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 ...
Aggiungeremo gli esempi condivisi "audibile creato", "contiene codice di errore" e "contiene messaggio di errore" 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 ...
Dobbiamo anche creare un modello audit_log:
rails g model audit_log backtrace data user:references rake db:migrate
Abbinare l'endpoint API di revisione della sessione di programmazione: risultati di ritorno
Per un utente autenticato e autorizzato, una chiamata a questo endpoint API dovrebbe restituire una serie di revisioni della sessione di programmazione di coppia raggruppate per progetto. Modifichiamo il nostro spec/api/pair_programming_spec.rb
conseguenza:
... context 'valid params' do it_behaves_like '200' it_behaves_like 'json result' end ...
Questo specifica che una richiesta inviata con api_key
valida e parametri validi restituisce un codice HTTP di 200 (ovvero successo) e che il risultato viene restituito sotto forma di JSON valido.
Esamineremo e quindi restituiremo in formato JSON quelle sessioni di programmazione di coppia in cui uno qualsiasi dei partecipanti è l'utente corrente ( 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 ...
Le sessioni di programmazione di coppia sono modellate come segue nel database:
- Relazione 1-a-molti tra progetti e sessioni di programmazione in coppia
- Relazione 1-a-molti tra sessioni di programmazione di coppia e revisioni
- Relazione 1-a-molti tra recensioni ed esempi di codice
Generiamo i modelli di conseguenza e quindi eseguiamo le migrazioni:
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
Quindi dobbiamo modificare le nostre classi PairProgrammingSession
e Review
per contenere le associazioni 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: in circostanze normali, genererei queste classi scrivendo prima le specifiche per esse, ma poiché ciò esula dallo scopo di questo articolo, salterò quel passaggio.
Ora dobbiamo scrivere quelle classi che trasformeranno i nostri modelli nelle loro rappresentazioni JSON (denominate entità uva nella terminologia dell'uva). Per semplicità, utilizzeremo la mappatura 1-a-1 tra i modelli e le entità uva.
Iniziamo esponendo il campo del code
da CodeSampleEntity
(in api/entities/code_sample_entity.rb
):
module Entities class CodeSampleEntity < Grape::Entity expose :code end end
Quindi esponiamo l' user
e i code_samples
associati riutilizzando UserEntity
e CodeSampleEntity
già definiti:
module Entities class ReviewEntity < Grape::Entity expose :user, using: UserEntity expose :code_samples, using: CodeSampleEntity end end
Esponiamo anche il campo del name
da ProjectEntity
:
module Entities class ProjectEntity < Grape::Entity expose :name end end
Infine, assembliamo l'entità in una nuova PairProgrammingSessionsEntity
in cui esponiamo il project
, l' host_user
, il visitor_user
e le 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 con ciò, la nostra API è completamente implementata!
Generazione di dati di prova
A scopo di test, creeremo alcuni dati di esempio in db/seeds.rb
. Questo file dovrebbe contenere tutta la creazione di record necessaria per eseguire il seeding del database con i suoi valori predefiniti. I dati possono quindi essere caricati con rake db:seed
(o creati con il db quando viene invocato db:setup
). Ecco un esempio di cosa potrebbe includere:
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'
Ora la nostra applicazione è pronta per l'uso e possiamo avviare il nostro server rails.
Testare l'API
Utilizzeremo Swagger per eseguire alcuni test manuali basati su browser della nostra API. Tuttavia, per poter utilizzare Swagger sono necessari alcuni passaggi di configurazione.
Per prima cosa, dobbiamo aggiungere un paio di gemme al nostro gemfile:
... gem 'grape-swagger' gem 'grape-swagger-ui' ...
Quindi eseguiamo bundle
per installare queste gemme.
Dobbiamo anche aggiungerli alle risorse nella nostra pipeline di risorse (in config/initializers/assets.rb
):
Rails.application.config.assets.precompile += %w( swagger_ui.js ) Rails.application.config.assets.precompile += %w( swagger_ui.css )
Infine, in app/api/api.rb
dobbiamo montare il generatore di spavalderia:
... add_swagger_documentation end ...
Ora possiamo sfruttare la bella interfaccia utente di Swagger per esplorare la nostra API semplicemente andando su http://localhost:3000/api/swagger
.
Swagger presenta i nostri endpoint API in un modo piacevolmente esplorabile. Se facciamo clic su un endpoint, Swagger elenca le sue operazioni. Se facciamo clic su un'operazione, Swagger mostra i suoi parametri obbligatori e facoltativi e i relativi tipi di dati.
Un dettaglio rimanente prima di procedere: poiché abbiamo limitato l'uso degli sviluppatori API con un api_key
valido, non saremo in grado di accedere all'endpoint API direttamente dal browser perché il server richiederà un api_key
valido nell'intestazione HTTP. Possiamo farlo a scopo di test in Google Chrome utilizzando il plug-in Modifica intestazioni per Google Chrome. Questo plugin ci consentirà di modificare l'intestazione HTTP e aggiungere una api_key
valida (useremo la api_key
fittizia di 12345654321
che abbiamo incluso nel file seme del nostro database).
OK, ora siamo pronti per il test!
Per chiamare l'endpoint dell'API pair_programming_sessions
, dobbiamo prima accedere. Utilizzeremo semplicemente le combinazioni di e-mail e password dal nostro file seme del database e lo invieremo tramite Swagger all'endpoint dell'API di accesso, come mostrato di seguito.
Come puoi vedere sopra, viene restituito il token appartenente a quell'utente, indicando che l'API di accesso funziona correttamente come previsto. Ora possiamo utilizzare quel token per eseguire correttamente l'operazione GET /api/pair_programming_sessions.json
.
Come mostrato, il risultato viene restituito come un oggetto JSON gerarchico correttamente formattato. Si noti che la struttura JSON riflette due associazioni nidificate da 1 a molti, poiché il progetto ha più revisioni e una revisione ha più esempi di codice. Se non dovessimo restituire la struttura in questo modo, il chiamante della nostra API dovrebbe richiedere separatamente le revisioni per ogni progetto che richiederebbe l'invio di N query al nostro endpoint API. Con questa struttura, risolviamo quindi il problema delle prestazioni delle query N+1.
Incartare
Come mostrato di seguito, le specifiche complete per l'API aiutano a garantire che l'API implementata risponda correttamente e adeguatamente ai casi d'uso previsti (e non previsti!).
Sebbene l'API di esempio presentata in questo tutorial sia piuttosto semplice, l'approccio e le tecniche che abbiamo dimostrato possono servire come base per API più sofisticate di complessità arbitraria utilizzando la gemma Grape. Si spera che questo tutorial abbia dimostrato che Grape è una gemma utile e flessibile che può aiutare a facilitare l'implementazione di un'API JSON nelle tue applicazioni Rails. Divertiti!