Tutorial Grape Gem: Cum să construiți un API asemănător REST în Ruby

Publicat: 2022-03-11

În calitate de dezvoltatori Ruby On Rails, de multe ori trebuie să ne extindem aplicațiile cu puncte finale API pentru a suporta clienți Rich Internet care folosesc JavaScript sau aplicațiile native pentru iPhone și Android. Există, de asemenea, unele cazuri în care singurul scop al aplicației este de a servi aplicațiile iPhone/Android printr-un API JSON.

În acest tutorial, demonstrez cum să utilizați Grape – un micro-cadru API asemănător REST pentru Ruby – pentru a construi suport backend în Rails pentru un API JSON. Grape este proiectat să ruleze ca un motor de rack montabil pentru a completa aplicațiile noastre web, fără a interfera cu acestea.

API web în Ruby folosind Grape Gem

Utilizare caz

Cazul de utilizare pe care ne vom concentra pentru acest tutorial este o aplicație capabilă să capteze și să revizuiască sesiuni de programare în pereche. Aplicația în sine va fi scrisă pentru iOS în ObjectiveC și va trebui să comunice cu un serviciu de backend pentru stocarea și preluarea datelor. Accentul nostru în acest tutorial este pe crearea unui serviciu backend robust și sigur care acceptă un API JSON.

API-ul va suporta metode pentru:

  • Conectarea la sistem
  • Interogarea recenziilor sesiunilor de programare perechi

NOTĂ: Pe lângă posibilitatea de a interoga recenziile sesiunilor de programare perechi, API-ul real ar trebui să ofere și o facilitate pentru trimiterea recenziilor de programare a perechilor pentru includerea în baza de date. Deoarece susținerea prin intermediul API-ului depășește scopul acestui tutorial, vom presupune pur și simplu că baza de date a fost populată cu un set de mostre de recenzii de programare perechi.

Cerințele tehnice cheie includ:

  • Fiecare apel API trebuie să returneze JSON valid
  • Fiecare apel API eșuat trebuie înregistrat cu context și informații adecvate pentru a fi ulterior reproductibil și depanat dacă este necesar

De asemenea, deoarece aplicația noastră va trebui să deservească clienții externi, va trebui să ne preocupăm de securitate. În acest scop:

  • Fiecare solicitare ar trebui să fie limitată la un mic subset de dezvoltatori pe care îi urmărim
  • Toate solicitările (altele decât autentificarea/înscrierea) trebuie să fie autentificate

Dezvoltare bazată pe teste și RSpec

Vom folosi Test Driven Development (TDD) ca abordare a dezvoltării software pentru a ne ajuta să ne asigurăm comportamentul determinist al API-ului nostru.

În scopuri de testare vom folosi RSpec, un cadru de testare bine cunoscut în comunitatea RubyOnRails. Prin urmare, în acest articol mă voi referi la „specificații” mai degrabă decât la „teste”.

O metodologie cuprinzătoare de testare constă atât din teste „pozitive”, cât și „negative”. Specificațiile negative vor specifica, de exemplu, cum se comportă punctul final API dacă unii parametri lipsesc sau sunt incorecți. Specificațiile pozitive acoperă cazurile în care API-ul este invocat corect.

Noțiuni de bază

Să punem bazele API-ului nostru de backend. Mai întâi, trebuie să creăm o nouă aplicație de șine:

 rails new toptal_grape_blog

În continuare, vom instala RSpec adăugând rspec-rails în fișierul nostru gem:

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

Apoi, din linia noastră de comandă trebuie să rulăm:

 rails generate rspec:install

De asemenea, putem folosi unele software-uri open source existente pentru cadrul nostru de testare. Specific:

  • Devise - o soluție flexibilă de autentificare pentru Rails bazată pe Warden
  • factory_girl_rails - oferă integrarea Rails pentru factory_girl, o bibliotecă pentru configurarea obiectelor Ruby ca date de testare

Pasul 1: Adăugați acestea în fișierul nostru gem:

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

Pasul 2: Generați un model de utilizator, inițializați bijuteria devise și adăugați-l la modelul utilizatorului (acest lucru permite ca clasa de utilizator să fie utilizată pentru autentificare).

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

Pasul 3: Includeți metoda de sintaxă factory_girl în fișierul nostru rails_helper.rb pentru a utiliza versiunea prescurtată a creării utilizatorilor în specificațiile noastre:

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

Pasul 4: Adăugați bijuteria de struguri la DSL-ul nostru și instalați-o:

 gem 'grape' bundle

Autentificare utilizator Caz de utilizare și specificații

Backend-ul nostru va trebui să accepte o capacitate de conectare de bază. Să creăm scheletul pentru login_spec , presupunând că o solicitare validă de conectare constă dintr-o adresă de e-mail înregistrată și o pereche de parolă:

 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

Dacă vreunul dintre parametri lipsește, clientul ar trebui să primească un cod de stare de returnare HTTP de 400 (adică Solicitare greșită), împreună cu un mesaj de eroare „lipsește e-mail” sau „lipsește parola”.

Pentru testul nostru, vom crea un utilizator valid și vom seta adresa de e-mail și parola utilizatorului ca parametri originali pentru această suită de teste. Apoi vom personaliza acest parametru hash pentru fiecare specificație specifică fie omițând parola/e-mailul, fie suprascriind-o.

Să creăm utilizatorul și parametrul hash la începutul specificației. Vom pune acest cod după blocul descris:

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

Ne putem extinde apoi contextul „parametrii lipsă”/„parola” după cum urmează:

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

Dar în loc să repetăm ​​așteptările în contextele „e-mail” și „parolă”, putem folosi aceleași exemple comune ca așteptări. Pentru aceasta, trebuie să decomentăm această linie din fișierul nostru rails_helper.rb :

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

Apoi trebuie să adăugăm cele 3 exemple RSpec partajate în 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

Aceste exemple comune apelează metoda api_call care ne permite să definim punctul final API o singură dată în specificațiile noastre (în conformitate cu principiul DRY). Definim aceasta metoda dupa cum urmeaza:

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

De asemenea, va trebui să personalizăm fabrica pentru utilizatorul nostru:

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

Și, în sfârșit, înainte de a ne rula specificațiile, trebuie să ne rulăm migrațiile:

 rake db:migrate

Rețineți, totuși, că specificațiile încă vor eșua în acest moment, deoarece nu am implementat încă punctul nostru final API. Urmează.

Implementarea API-ului de conectare Endpoint

Pentru început, vom scrie un schelet gol pentru API-ul nostru de conectare ( 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

În continuare, vom scrie o clasă de agregare care agregează punctele finale API ( app/api/api.rb ):

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

OK, acum putem monta API-ul nostru în rutele:

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

Acum să adăugăm codul pentru a verifica parametrii lipsă. Putem adăuga acel cod la api.rb prin salvarea din Grape::Exceptions::ValidationErrors .

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

Pentru parola nevalidă, vom verifica dacă codul de răspuns http este 401, ceea ce înseamnă acces neautorizat. Să adăugăm asta la contextul „parolă incorectă”:

 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'

Aceeași logică este apoi adăugată și contextului „cu o conectare inexistentă”.

Apoi implementăm logica care gestionează încercările nevalide de autentificare în login.rb , după cum urmează:

 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

În acest moment, toate specificațiile negative pentru API-ul de conectare se vor comporta corect, dar încă trebuie să acceptăm specificațiile pozitive pentru API-ul nostru de conectare. Specificația noastră pozitivă se va aștepta ca punctul final să returneze un cod de răspuns HTTP de 200 (adică, succes) cu JSON valid și un token valid:

 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

Să adăugăm, de asemenea, așteptarea pentru codul de răspuns 200 la spec/support/shared.rb :

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

În cazul autentificării cu succes, vom returna primul authentication_token valid împreună cu e-mailul utilizatorului în acest format:

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

Dacă nu există încă un astfel de simbol, atunci vom crea unul pentru utilizatorul actual:

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

Pentru ca acest lucru să funcționeze, vom avea nevoie de o clasă AuthenticationToken care aparține utilizatorului. Vom genera acest model, apoi vom rula migrarea corespunzătoare:

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

De asemenea, trebuie să adăugăm asocierea corespunzătoare modelului nostru de utilizator:

 class User < ActiveRecord::Base has_many :authentication_tokens end

Apoi vom adăuga domeniul valid la clasa AuthenticationToken :

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

Rețineți că am folosit sintaxa Ruby în instrucțiunea where . Acest lucru este activat prin utilizarea noastră a bijuteriei squeel , care permite suportul pentru sintaxa Ruby în interogările activerecord.

Pentru un utilizator validat, vom crea o entitate la care ne vom referi ca „utilizator cu entitate token”, valorificând caracteristicile bijuteriei grape-entity .

Să scriem specificațiile pentru entitatea noastră și să o punem în fișierul 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

În continuare, să adăugăm entitățile la user_entity.rb :

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

Și, în sfârșit, adăugați o altă clasă la user_with_token_entity.rb :

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

Deoarece nu dorim ca token-urile să rămână valabile pe termen nelimitat, le facem să expire după o zi:

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

Cu toate acestea gata, acum putem returna formatul JSON așteptat cu UserWithTokenEntity nou scris:

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

Misto. Acum toate specificațiile noastre trec și cerințele funcționale ale punctului final api de conectare de bază sunt acceptate.

Punct final API de examinare a sesiunii de programare perechi: Noțiuni introductive

Backend-ul nostru va trebui să permită dezvoltatorilor autorizați care s-au autentificat să interogă recenzii ale sesiunilor de programare perechi.

Noul nostru punct final API va fi montat pe /api/pair_programming_session și va returna recenziile aparținând unui proiect. Să începem prin a scrie un schelet de bază pentru această specificație:

 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

Vom scrie și un punct final API gol corespunzător ( 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

Apoi, să instalăm noua noastră API ( app/api/api.rb ):

 ... mount Login mount PairProgrammingSessions end

Să extindem specificațiile și punctul final API, în raport cu cerințele, unul câte unul.

Punct final API de revizuire a sesiunii de programare pereche: validare

Una dintre cele mai importante cerințe de securitate nefuncțională a fost restricționarea accesului la API la un subset mic de dezvoltatori pe care îi urmărim, așa că să specificăm că:

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

Apoi vom crea un shared_example în shared.rb pentru a confirma că solicitarea vine de la unul dintre dezvoltatorii noștri înregistrați:

 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

De asemenea, va trebui să creăm o clasă ErrorCodes (în app/models/error_codes.rb ):

 module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end

Deoarece ne așteptăm ca API-ul nostru să se extindă în viitor, vom implementa un authorization_helper care poate fi reutilizat în toate punctele finale API din aplicație pentru a restricționa accesul numai dezvoltatorilor înregistrați:

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

Vom defini metoda restrict_access_to_developers în modulul ApiHelpers::AuthenticationHerlper ( app/api/api_helpers/authentication_helper.rb ). Această metodă va verifica pur și simplu dacă cheia Authorization sub anteturi conține o ApiKey validă. (Fiecare dezvoltator care dorește acces la API va avea nevoie de o ApiKey validă. Aceasta poate fi furnizată fie de un administrator de sistem, fie printr-un proces automat de înregistrare, dar acest mecanism depășește domeniul de aplicare al acestui articol.)

 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

Apoi trebuie să generăm modelul ApiKey și să rulăm migrațiile: rails g model api_key token rake db:migrate

Cu acest lucru făcut, în spec/api/pair_programming_spec.rb nostru putem verifica dacă utilizatorul este autentificat:

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

Să definim, de asemenea, un exemplu partajat unauthenticated care poate fi reutilizat pentru toate specificațiile ( 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

Acest exemplu partajat are nevoie de simbolul din antetul dezvoltatorului, așa că să adăugăm asta la specificațiile noastre ( spec/api/pair_programming_spec.rb ):

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

Acum, în app/api/pair_programming_session.rb , să încercăm să autentificăm utilizatorul:

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

Să implementăm authenticate! metoda din 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 ...

(Rețineți că trebuie să adăugăm codul de eroare BAD_AUTHENTICATION_PARAMS la clasa noastră ErrorCodes .)

Apoi, să specificăm ce se întâmplă dacă dezvoltatorul apelează API-ul cu un token nevalid. În acest caz, codul de retur va fi 401 care semnalează un „acces neautorizat”. Rezultatul ar trebui să fie JSON și ar trebui creat un auditabil. Așa că adăugăm asta la 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 ...

Vom adăuga exemplele „auditable create”, „conține cod de eroare” și „conține mesaj de eroare” la 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 ...

De asemenea, trebuie să creăm un model audit_log:

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

Punct final API de examinare a sesiunii de programare pereche: returnarea rezultatelor

Pentru un utilizator autentificat și autorizat, un apel către acest punct final API ar trebui să returneze un set de recenzii ale sesiunii de programare perechi grupate în funcție de proiect. Să modificăm spec/api/pair_programming_spec.rb în consecință:

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

Aceasta specifică faptul că o solicitare trimisă cu api_key valide și parametri validi returnează un cod HTTP de 200 (adică succes) și că rezultatul este returnat sub formă de JSON valid.

Vom interoga și apoi vom returna în format JSON acele sesiuni de programare perechi în care oricare dintre participanti este utilizatorul curent ( 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 ...

Sesiunile de programare perechi sunt modelate după cum urmează în baza de date:

  • Relația de la 1 la mulți dintre proiecte și sesiuni de programare în pereche
  • Relația 1-la-mulți între sesiunile de programare în pereche și recenzii
  • Relația de la 1 la mulți dintre recenzii și mostre de cod

Să generăm modelele în consecință și apoi să rulăm migrațiile:

 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

Apoi trebuie să modificăm clasele PairProgrammingSession și Review pentru a conține asociațiile 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

NOTĂ: În circumstanțe normale, aș genera aceste clase scriind mai întâi specificațiile pentru ele, dar deoarece acest lucru depășește domeniul de aplicare al acestui articol, voi sări peste acel pas.

Acum trebuie să scriem acele clase care vor transforma modelele noastre în reprezentările lor JSON (denumite entități-struguri în terminologia strugurilor). Pentru simplitate, vom folosi maparea 1-la-1 între modele și entitățile de struguri.

Începem prin a expune câmpul de code din CodeSampleEntity (în api/entities/code_sample_entity.rb ):

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

Apoi expunem user și code_samples asociate prin reutilizarea CodeSampleEntity UserEntity

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

De asemenea, expunem câmpul de name din ProjectEntity :

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

În cele din urmă, asamblam entitatea într-o nouă entitate PairProgrammingSessionsEntity unde expunem project , host_user , visitor_user și 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

Și cu asta, API-ul nostru este pe deplin implementat!

Generarea datelor de testare

În scopuri de testare, vom crea câteva exemple de date în db/seeds.rb . Acest fișier ar trebui să conțină toată crearea de înregistrări necesară pentru a genera baza de date cu valorile implicite. Datele pot fi apoi încărcate cu rake db:seed (sau create cu db când este invocat db:setup ). Iată un exemplu despre ceea ce ar putea include acest lucru:

 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'

Acum aplicația noastră este gata de utilizare și putem lansa serverul nostru de șine.

Testarea API-ului

Vom folosi Swagger pentru a face niște teste manuale bazate pe browser ale API-ului nostru. Sunt necesari câțiva pași de configurare pentru ca noi să putem folosi Swagger.

Mai întâi, trebuie să adăugăm câteva pietre prețioase la fișierul nostru gem:

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

Apoi rulăm bundle pentru a instala aceste pietre prețioase.

De asemenea, trebuie să le adăugăm la active în conducta noastră de active (în config/initializers/assets.rb ):

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

În cele din urmă, în app/api/api.rb trebuie să instalăm generatorul de swagger:

 ... add_swagger_documentation end ...

Acum putem profita de interfața de utilizare frumoasă a lui Swagger pentru a explora API-ul nostru, pur și simplu accesând http://localhost:3000/api/swagger .

Swagger prezintă punctele noastre finale API într-un mod frumos explorabil. Dacă facem clic pe un punct final, Swagger listează operațiunile acestuia. Dacă facem clic pe o operație, Swagger arată parametrii necesari și opționali și tipurile lor de date.

Un detaliu rămas însă înainte de a continua: deoarece am restricționat utilizarea dezvoltatorilor API cu un api_key valid, nu vom putea accesa punctul final API direct din browser, deoarece serverul va necesita o api_key validă în antetul HTTP. Putem realiza acest lucru în scopuri de testare în Google Chrome utilizând pluginul Modificare anteturi pentru Google Chrome. Acest plugin ne va permite să edităm antetul HTTP și să adăugăm o api_key validă (vom folosi api_key a 12345654321 pe care am inclus-o în fișierul de bază de date).

OK, acum suntem gata de testare!

Pentru a apela punctul final API pair_programming_sessions , trebuie mai întâi să ne conectăm. Vom folosi doar combinațiile de e-mail și parole din fișierul de bază al bazei de date și le vom trimite prin Swagger la punctul final API de conectare, așa cum se arată mai jos.

După cum puteți vedea mai sus, token-ul aparținând acelui utilizator este returnat, indicând faptul că API-ul de conectare funcționează așa cum este prevăzut. Acum putem folosi acel token pentru a efectua cu succes GET /api/pair_programming_sessions.json .

După cum se arată, rezultatul este returnat ca obiect JSON ierarhic formatat corespunzător. Observați că structura JSON reflectă două asocieri imbricate de la 1 la mai multe, deoarece proiectul are mai multe recenzii, iar o revizuire are mai multe mostre de cod. Dacă nu am returna structura în acest fel, atunci apelantul API-ului nostru ar trebui să solicite separat recenziile pentru fiecare proiect, ceea ce ar necesita trimiterea de N interogări la punctul nostru final API. Prin această structură, rezolvăm problema de performanță a interogărilor N+1.

Învelire

După cum se arată aici, specificațiile complete pentru API-ul dvs. vă ajută să vă asigurați că API-ul implementat abordează în mod corespunzător și adecvat cazurile de utilizare intenționate (și neintenționate!).

În timp ce exemplul de API prezentat în acest tutorial este destul de simplu, abordarea și tehnicile pe care le-am demonstrat pot servi drept bază pentru API-uri mai sofisticate de complexitate arbitrară, folosind bijuteria Grape. Acest tutorial a demonstrat că Grape este o bijuterie utilă și flexibilă, care poate ajuta la facilitarea implementării unui API JSON în aplicațiile dumneavoastră Rails. Bucurați-vă!