Samouczek Grape Gem: Jak zbudować interfejs API podobny do REST w Ruby

Opublikowany: 2022-03-11

Jako programiści Ruby On Rails, często musimy rozszerzać nasze aplikacje o punkty końcowe API, aby obsługiwać klientów Rich Internet z dużą ilością JavaScript lub natywne aplikacje na iPhone'a i Androida. Istnieją również przypadki, w których jedynym celem aplikacji jest obsługa aplikacji na iPhone'a/Androida za pośrednictwem interfejsu API JSON.

W tym samouczku zademonstruję, jak używać Grape – mikro-frameworka API podobnego do REST dla Ruby – do budowania wsparcia backendu w Rails dla API JSON. Grape jest zaprojektowany do pracy jako silnik do montażu w stojaku, który uzupełnia nasze aplikacje internetowe, bez ingerencji w nie.

Web API w Ruby przy użyciu Grape Gem

Przypadek użycia

Przypadek użycia, na którym skupimy się w tym samouczku, to aplikacja zdolna do przechwytywania i przeglądania sesji programowania w parach. Sama aplikacja zostanie napisana dla iOS w ObjectiveC i będzie musiała komunikować się z usługą backendu w celu przechowywania i pobierania danych. W tym samouczku koncentrujemy się na tworzeniu niezawodnej i bezpiecznej usługi zaplecza obsługującej interfejs API JSON.

API będzie wspierać metody dla:

  • Logowanie do systemu
  • Odpytywanie recenzji sesji programowania par

UWAGA: Oprócz możliwości wysyłania zapytań o recenzje sesji programowania w parach, prawdziwy interfejs API musiałby również zapewniać możliwość przesyłania recenzji programowania w parach do włączenia do bazy danych. Ponieważ obsługa tego za pośrednictwem interfejsu API wykracza poza zakres tego samouczka, po prostu założymy, że baza danych została wypełniona przykładowym zestawem recenzji programowania w parach.

Kluczowe wymagania techniczne obejmują:

  • Każde wywołanie API musi zwrócić prawidłowy JSON
  • Każde nieudane wywołanie API musi być rejestrowane z odpowiednim kontekstem i informacjami, aby można je było później odtworzyć i w razie potrzeby debugować

Ponadto, ponieważ nasza aplikacja będzie musiała obsługiwać klientów zewnętrznych, będziemy musieli zadbać o bezpieczeństwo. W tym celu:

  • Każde żądanie powinno być ograniczone do małego podzbioru programistów, których śledzimy
  • Wszystkie żądania (poza logowaniem/rejestracją) muszą być uwierzytelnione

Rozwój oparty na testach i RSpec

Wykorzystamy Test Driven Development (TDD) jako nasze podejście do tworzenia oprogramowania, aby zapewnić deterministyczne zachowanie naszego interfejsu API.

Do celów testowych użyjemy RSpec, dobrze znanego frameworka testowego w społeczności RubyOnRails. Dlatego w tym artykule będę odnosić się do „specyfikacji”, a nie „testów”.

Kompleksowa metodologia testowania składa się zarówno z testów „pozytywnych”, jak i „negatywnych”. Specyfikacje negatywne określą na przykład, jak zachowuje się punkt końcowy interfejsu API, jeśli brakuje niektórych parametrów lub są one nieprawidłowe. Pozytywne specyfikacje obejmują przypadki, w których interfejs API jest wywoływany poprawnie.

Pierwsze kroki

Połóżmy fundament pod nasz backend API. Najpierw musimy stworzyć nową aplikację rails:

 rails new toptal_grape_blog

Następnie zainstalujemy RSpec, dodając rspec-rails do naszego pliku gemfile:

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

Następnie z naszego wiersza poleceń musimy uruchomić:

 rails generate rspec:install

Możemy również wykorzystać istniejące oprogramowanie open source do naszych ram testowych. Konkretnie:

  • Devise - elastyczne rozwiązanie uwierzytelniające dla Rails oparte na Warden
  • factory_girl_rails - zapewnia integrację Rails z factory_girl, biblioteką do konfigurowania obiektów Ruby jako danych testowych

Krok 1: Dodaj je do naszego pliku gemfile:

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

Krok 2: Wygeneruj model użytkownika, zainicjuj klejnot devise i dodaj go do modelu użytkownika (pozwala to na użycie klasy użytkownika do uwierzytelniania).

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

Krok 3: Dołącz metodę składni factory_girl do naszego pliku rails_helper.rb , aby użyć skróconej wersji tworzenia użytkownika w naszej specyfikacji:

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

Krok 4: Dodaj winogronowy klejnot do naszego DSL i zainstaluj go:

 gem 'grape' bundle

Przypadek użycia i specyfikacja logowania użytkownika

Nasz backend będzie musiał obsługiwać podstawową funkcję logowania. Stwórzmy szkielet dla naszego login_spec , zakładając, że prawidłowe żądanie logowania składa się z zarejestrowanego adresu e-mail i pary haseł:

 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

Jeśli brakuje któregokolwiek z parametrów, klient powinien otrzymać zwrotny kod statusu HTTP 400 (tj. Złe żądanie) wraz z komunikatem o błędzie „brak e-maila” lub „brak hasła”.

Na potrzeby naszego testu utworzymy prawidłowego użytkownika i ustawimy adres e-mail i hasło użytkownika jako oryginalne parametry dla tego zestawu testów. Następnie dostosujemy ten skrót parametru dla każdej konkretnej specyfikacji, pomijając hasło/e-mail lub nadpisując je.

Utwórzmy użytkownika i hash parametru na początku specyfikacji. Umieścimy ten kod po bloku opisu:

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

Następnie możemy rozszerzyć nasz kontekst „brakujące parametry”/„hasło” w następujący sposób:

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

Ale zamiast powtarzać oczekiwania w kontekście „e-mail” i „hasło”, możemy użyć tych samych wspólnych przykładów, co oczekiwania. W tym celu musimy odkomentować tę linię w naszym pliku rails_helper.rb :

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

Następnie musimy dodać 3 współdzielone przykłady RSpec do 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

Te wspólne przykłady wywołują metodę api_call , która umożliwia nam zdefiniowanie punktu końcowego API tylko raz w naszej specyfikacji (zgodnie z zasadą DRY). Definiujemy tę metodę w następujący sposób:

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

Będziemy również musieli dostosować fabrykę dla naszego użytkownika:

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

I na koniec, przed uruchomieniem naszych specyfikacji, musimy uruchomić nasze migracje:

 rake db:migrate

Pamiętaj jednak, że specyfikacje nadal nie będą działać w tym momencie, ponieważ nie zaimplementowaliśmy jeszcze naszego punktu końcowego API. To jest następne.

Implementacja punktu końcowego API logowania

Na początek napiszemy pusty szkielet dla naszego API logowania ( 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

Następnie napiszemy klasę agregatora, która agreguje punkty końcowe API ( app/api/api.rb ):

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

OK, teraz możemy zamontować nasze API w trasach:

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

Teraz dodajmy kod, aby sprawdzić brakujące parametry. Możemy dodać ten kod do api.rb , ratując się przed Grape::Exceptions::ValidationErrors .

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

W przypadku nieprawidłowego hasła sprawdzimy, czy kod odpowiedzi http to 401, co oznacza nieautoryzowany dostęp. Dodajmy to do kontekstu „niepoprawne hasło”:

 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'

Ta sama logika jest następnie dodawana do kontekstu „z nieistniejącym loginem”.

Następnie implementujemy logikę, która obsługuje nieprawidłowe próby uwierzytelnienia w naszym login.rb w następujący sposób:

 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

W tym momencie wszystkie negatywne specyfikacje interfejsu API logowania będą zachowywać się prawidłowo, ale nadal musimy obsługiwać pozytywne specyfikacje interfejsu API logowania. Nasza pozytywna specyfikacja będzie oczekiwać, że punkt końcowy zwróci kod odpowiedzi HTTP o wartości 200 (tj. sukces) z poprawnym JSON i poprawnym tokenem:

 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

Dodajmy również oczekiwanie na kod odpowiedzi 200 do spec/support/shared.rb :

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

W przypadku udanego logowania zwrócimy pierwszy ważny token uwierzytelniania wraz z e-mailem użytkownika w następującym formacie:

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

Jeśli takiego tokena jeszcze nie ma, utworzymy go dla aktualnego użytkownika:

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

Aby to zadziałało, będziemy potrzebować klasy AuthenticationToken , która należy do użytkownika. Wygenerujemy ten model, a następnie uruchomimy odpowiednią migrację:

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

Musimy również dodać odpowiednie powiązanie do naszego modelu użytkownika:

 class User < ActiveRecord::Base has_many :authentication_tokens end

Następnie dodamy prawidłowy zakres do klasy AuthenticationToken :

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

Zauważ, że w instrukcji where użyliśmy składni Rubiego. Jest to możliwe dzięki użyciu gem squeel , który umożliwia obsługę składni Rubiego w zapytaniach activerecord.

Dla zweryfikowanego użytkownika stworzymy encję, którą będziemy nazywać „użytkownikiem z tokenem”, wykorzystując cechy klejnotu grape-entity .

Napiszmy specyfikację naszej encji i umieśćmy ją w pliku 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

Następnie dodajmy encje do user_entity.rb :

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

I na koniec dodaj kolejną klasę do user_with_token_entity.rb :

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

Ponieważ nie chcemy, aby tokeny pozostawały ważne w nieskończoność, wygasają one po jednym dniu:

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

Po wykonaniu tych wszystkich czynności możemy teraz zwrócić oczekiwany format JSON z naszym nowo napisanym 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 ...

Fajny. Teraz wszystkie nasze specyfikacje mijają, a wymagania funkcjonalne podstawowego punktu końcowego API logowania są obsługiwane.

Punkt końcowy interfejsu API przeglądania sesji programowania w parach: Pierwsze kroki

Nasz backend będzie musiał umożliwić autoryzowanym programistom, którzy się zalogowali, wysyłanie zapytań o przeglądy sesji programowania w parach.

Nasz nowy punkt końcowy API zostanie podłączony do /api/pair_programming_session i zwróci recenzje należące do projektu. Zacznijmy od napisania podstawowego szkieletu dla tej specyfikacji:

 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

Napiszemy również odpowiedni pusty punkt końcowy 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

Następnie zamontujmy nasze nowe api ( app/api/api.rb ):

 ... mount Login mount PairProgrammingSessions end

Rozszerzmy specyfikację i punkt końcowy API, jeden po drugim, pod kątem wymagań.

Punkt końcowy interfejsu API przeglądania sesji programowania par: weryfikacja

Jednym z naszych najważniejszych wymagań dotyczących bezpieczeństwa niefunkcjonalnego było ograniczenie dostępu API do niewielkiej grupy programistów, których śledzimy, więc określmy to:

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

Następnie utworzymy shared_example w naszym shared.rb , aby potwierdzić, że żądanie pochodzi od jednego z naszych zarejestrowanych programistów:

 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

Będziemy również musieli utworzyć klasę ErrorCodes (w app/models/error_codes.rb ):

 module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end

Ponieważ spodziewamy się, że nasze API będzie się rozwijać w przyszłości, zamierzamy zaimplementować authorization_helper , która może być ponownie wykorzystana we wszystkich punktach końcowych API w aplikacji, aby ograniczyć dostęp tylko do zarejestrowanych programistów:

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

Zdefiniujemy metodę limited_access_to_developers w module restrict_access_to_developers ApiHelpers::AuthenticationHerlper ( app/api/api_helpers/authentication_helper.rb ). Ta metoda po prostu sprawdzi, czy klucz Authorization pod nagłówkami zawiera poprawny ApiKey . (Każdy programista, który chce uzyskać dostęp do interfejsu API, będzie wymagał prawidłowego ApiKey . Może on być dostarczony przez administratora systemu lub za pośrednictwem zautomatyzowanego procesu rejestracji, ale ten mechanizm wykracza poza zakres tego artykułu.)

 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

Następnie musimy wygenerować model ApiKey i uruchomić migracje: rails model g api_key token rake db:migrate

Po wykonaniu tej czynności w naszym spec/api/pair_programming_spec.rb możemy sprawdzić, czy użytkownik jest uwierzytelniony:

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

Zdefiniujmy również unauthenticated wspólny przykład, który można ponownie wykorzystać we wszystkich specyfikacjach ( 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

Ten wspólny przykład wymaga tokena w nagłówku dewelopera, więc dodajmy go do naszej specyfikacji ( spec/api/pair_programming_spec.rb ):

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

Teraz w naszym app/api/pair_programming_session.rb spróbujmy uwierzytelnić użytkownika:

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

Zaimplementujmy authenticate! metoda w 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 ...

(Pamiętaj, że musimy dodać kod błędu BAD_AUTHENTICATION_PARAMS do naszej klasy ErrorCodes ).

Następnie sprecyzujmy, co się stanie, jeśli deweloper wywoła API z nieprawidłowym tokenem. W takim przypadku kodem zwrotnym będzie 401 sygnalizujący „nieautoryzowany dostęp”. Wynik powinien być w formacie JSON i powinien zostać utworzony audytowalny. Więc dodajemy to do 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 ...

Dodamy wspólne przykłady „utworzony do audytu”, „zawiera kod błędu” i „zawiera komunikat o błędzie” do 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 ...

Musimy również stworzyć model audit_log:

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

Punkt końcowy interfejsu API przeglądania sesji programowania w parach: zwracanie wyników

W przypadku uwierzytelnionego i autoryzowanego użytkownika wywołanie tego punktu końcowego interfejsu API powinno zwrócić zestaw przeglądów sesji programowania par pogrupowanych według projektu. Zmodyfikujmy odpowiednio nasz spec/api/pair_programming_spec.rb :

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

Oznacza to, że żądanie przesłane z prawidłowym api_key i prawidłowymi parametrami zwraca kod HTTP o wartości 200 (tj. sukces), a wynik jest zwracany w postaci prawidłowego JSON.

Zamierzamy zapytać, a następnie zwrócimy w formacie JSON te sesje programowania par, w których którykolwiek z uczestników jest bieżącym użytkownikiem ( 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 ...

Sesje programowania w parach są modelowane w bazie danych w następujący sposób:

  • Relacja 1 do wielu między projektami i sesjami programowania w parach
  • Relacja jeden-do-wielu między sesjami programowania w parach a recenzjami
  • Relacja 1 do wielu między recenzjami a próbkami kodu

Wygenerujmy odpowiednio modele, a następnie uruchommy migracje:

 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

Następnie musimy zmodyfikować nasze PairProgrammingSession i Review , aby zawierały asocjacje 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

UWAGA: W normalnych okolicznościach wygenerowałbym te klasy, najpierw pisząc dla nich specyfikacje, ale ponieważ wykracza to poza zakres tego artykułu, pominę ten krok.

Teraz musimy napisać te klasy, które przekształcą nasze modele na ich reprezentacje JSON (określane w terminologii winogron jako encje winogron). Dla uproszczenia użyjemy mapowania 1 do 1 między modelami i jednostkami winogron.

Zaczynamy od ujawnienia pola code z CodeSampleEntity (w api/entities/code_sample_entity.rb ):

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

Następnie uwidaczniamy user i powiązane code_samples , ponownie wykorzystując już zdefiniowane UserEntity i CodeSampleEntity :

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

Udostępniamy również pole name z ProjectEntity :

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

Na koniec składamy jednostkę w nowy PairProgrammingSessionsEntity , w którym udostępniamy 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 dzięki temu nasze API jest w pełni zaimplementowane!

Generowanie danych testowych

Do celów testowych utworzymy kilka przykładowych danych w db/seeds.rb . Ten plik powinien zawierać wszystkie utworzone rekordy potrzebne do obsadzenia bazy danych jej wartościami domyślnymi. Dane można następnie załadować za pomocą rake db:seed (lub utworzyć za pomocą db, gdy wywoływana jest db:setup ). Oto przykład tego, co może to obejmować:

 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'

Teraz nasza aplikacja jest gotowa do użycia i możemy uruchomić nasz serwer rails.

Testowanie API

Wykorzystamy Swagger do ręcznego testowania naszego interfejsu API w przeglądarce. Wymagane jest jednak kilka kroków konfiguracji, abyśmy mogli korzystać ze Swaggera.

Najpierw musimy dodać kilka klejnotów do naszego pliku gemfile:

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

Następnie uruchamiamy bundle , aby zainstalować te klejnoty.

Musimy również dodać je do zasobów do naszego potoku zasobów (w config/initializers/assets.rb ):

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

Na koniec w app/api/api.rb musimy zamontować generator swaggerów:

 ... add_swagger_documentation end ...

Teraz możemy skorzystać z ładnego interfejsu użytkownika Swaggera, aby zapoznać się z naszym interfejsem API, przechodząc po prostu pod http://localhost:3000/api/swagger .

Swagger prezentuje nasze punkty końcowe API w ładnie eksplorowany sposób. Jeśli klikniemy punkt końcowy, Swagger wyświetli listę jego operacji. Jeśli klikniemy operację, Swagger pokazuje wymagane i opcjonalne parametry oraz ich typy danych.

Jeszcze jeden szczegół, zanim przejdziemy dalej: ponieważ ograniczyliśmy korzystanie z programistów API za pomocą prawidłowego api_key , nie będziemy mogli uzyskać dostępu do punktu końcowego interfejsu API bezpośrednio z przeglądarki, ponieważ serwer będzie wymagał prawidłowego api_key w nagłówku HTTP. Możemy to zrobić w celach testowych w Google Chrome, korzystając z wtyczki Modify Headers for Google Chrome. Ta wtyczka umożliwi nam edycję nagłówka HTTP i dodanie prawidłowego api_key (użyjemy fikcyjnego api_key 12345654321 , który zawarliśmy w naszym pliku źródłowym bazy danych).

OK, teraz jesteśmy gotowi do testowania!

Aby wywołać punkt końcowy API pair_programming_sessions , najpierw musimy się zalogować. Po prostu użyjemy kombinacji adresu e-mail i hasła z naszego pliku źródłowego bazy danych i prześlemy je za pomocą Swaggera do punktu końcowego API logowania, jak pokazano poniżej.

Jak widać powyżej, token należący do tego użytkownika jest zwracany, co wskazuje, że interfejs API logowania działa prawidłowo, zgodnie z przeznaczeniem. Możemy teraz użyć tego tokena do pomyślnego wykonania operacji GET /api/pair_programming_sessions.json .

Jak pokazano, wynik jest zwracany jako odpowiednio sformatowany hierarchiczny obiekt JSON. Zwróć uwagę, że struktura JSON odzwierciedla dwa zagnieżdżone skojarzenia 1-do-wielu, ponieważ projekt ma wiele recenzji, a recenzja zawiera wiele próbek kodu. Gdybyśmy nie zwracali struktury w ten sposób, to osoba wywołująca naszego API musiałaby osobno zażądać recenzji dla każdego projektu, co wymagałoby przesłania N zapytań do naszego punktu końcowego API. Dzięki tej strukturze rozwiązujemy zatem problem z wydajnością zapytań N+1.

Zakończyć

Jak pokazano w niniejszym dokumencie, wyczerpujące specyfikacje Twojego interfejsu API pomagają zapewnić, że zaimplementowany interfejs API prawidłowo i odpowiednio odnosi się do zamierzonych (i niezamierzonych!) przypadków użycia.

Chociaż przykładowy interfejs API przedstawiony w tym samouczku jest dość prosty, podejście i techniki, które zademonstrowaliśmy, mogą służyć jako podstawa bardziej wyrafinowanych interfejsów API o dowolnej złożoności przy użyciu klejnotu Grape. Ten samouczek, miejmy nadzieję, pokazał, że Grape jest użytecznym i elastycznym klejnotem, który może ułatwić implementację API JSON w twoich aplikacjach Railsowych. Cieszyć się!