Grape Gem Tutorial: So erstellen Sie eine REST-ähnliche API in Ruby

Veröffentlicht: 2022-03-11

Als Ruby On Rails-Entwickler müssen wir unsere Anwendungen oft mit API-Endpunkten erweitern, um JavaScript-lastige Rich-Internet-Clients oder native iPhone- und Android-Apps zu unterstützen. Es gibt auch einige Fälle, in denen der einzige Zweck der Anwendung darin besteht, die iPhone/Android-Apps über eine JSON-API bereitzustellen.

In diesem Tutorial zeige ich, wie man Grape – ein REST-ähnliches API-Mikro-Framework für Ruby – verwendet, um Backend-Unterstützung in Rails für eine JSON-API zu erstellen. Grape ist so konzipiert, dass es als montierbare Rack-Engine läuft, um unsere Webanwendungen zu ergänzen , ohne sie zu stören .

Web-API in Ruby mit Grape Gem

Anwendungsfall

Der Anwendungsfall, auf den wir uns in diesem Tutorial konzentrieren werden, ist eine Anwendung, die Paarprogrammierungssitzungen erfassen und überprüfen kann. Die Anwendung selbst wird für iOS in ObjectiveC geschrieben und muss mit einem Backend-Dienst kommunizieren, um die Daten zu speichern und abzurufen. Unser Fokus in diesem Tutorial liegt auf der Erstellung eines robusten und sicheren Back-End-Dienstes, der eine JSON-API unterstützt.

Die API unterstützt Methoden für:

  • Einloggen in das System
  • Abfragen von Sitzungsüberprüfungen zur Paarprogrammierung

ANMERKUNG: Zusätzlich zur Bereitstellung der Möglichkeit, Paarprogrammierungssitzungsüberprüfungen abzufragen, müsste die echte API auch eine Einrichtung zum Einreichen von Paarprogrammierungsüberprüfungen zur Aufnahme in die Datenbank bereitstellen. Da die Unterstützung über die API den Rahmen dieses Tutorials sprengen würde, gehen wir einfach davon aus, dass die Datenbank mit einem Beispielsatz von Pair Programming Reviews gefüllt wurde.

Zu den wichtigsten technischen Anforderungen gehören:

  • Jeder API-Aufruf muss gültiges JSON zurückgeben
  • Jeder fehlgeschlagene API-Aufruf muss mit angemessenem Kontext und Informationen aufgezeichnet werden, um anschließend reproduzierbar zu sein und bei Bedarf zu debuggen

Da unsere Anwendung externe Clients bedienen muss, müssen wir uns auch um die Sicherheit kümmern. Dazu:

  • Jede Anfrage sollte auf eine kleine Untergruppe von Entwicklern beschränkt sein, die wir verfolgen
  • Alle Anfragen (außer Anmeldung/Anmeldung) müssen authentifiziert werden

Testgetriebene Entwicklung und RSpec

Wir werden Test Driven Development (TDD) als unseren Softwareentwicklungsansatz verwenden, um das deterministische Verhalten unserer API sicherzustellen.

Zu Testzwecken verwenden wir RSpec, ein bekanntes Testframework in der RubyOnRails-Community. Ich werde daher in diesem Artikel eher von „Spezifikationen“ als von „Tests“ sprechen.

Eine umfassende Testmethodik besteht aus „positiven“ und „negativen“ Tests. Negative Spezifikationen geben beispielsweise an, wie sich der API-Endpunkt verhält, wenn einige Parameter fehlen oder falsch sind. Positive Spezifikationen decken Fälle ab, in denen die API korrekt aufgerufen wird.

Einstieg

Lassen Sie uns die Grundlage für unsere Back-End-API legen. Zuerst müssen wir eine neue Schienenanwendung erstellen:

 rails new toptal_grape_blog

Als Nächstes installieren wir RSpec, indem wir rspec-rails zu unserer Gemfile hinzufügen:

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

Dann müssen wir von unserer Befehlszeile aus Folgendes ausführen:

 rails generate rspec:install

Wir können auch einige vorhandene Open-Source-Software für unser Test-Framework nutzen. Speziell:

  • Devise – eine flexible Authentifizierungslösung für Rails basierend auf Warden
  • factory_girl_rails – bietet Rails-Integration für factory_girl, eine Bibliothek zum Einrichten von Ruby-Objekten als Testdaten

Schritt 1: Fügen Sie diese in unser Gemfile ein:

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

Schritt 2: Generieren Sie ein Benutzermodell, initialisieren Sie das devise -Gem und fügen Sie es dem Benutzermodell hinzu (dadurch kann die Benutzerklasse zur Authentifizierung verwendet werden).

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

Schritt 3: Fügen Sie die factory_girl in unsere Datei rails_helper.rb ein, um die abgekürzte Version der Benutzererstellung in unseren Spezifikationen zu verwenden:

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

Schritt 4: Fügen Sie das Grape Gem zu unserer DSL hinzu und installieren Sie es:

 gem 'grape' bundle

Anwendungsfall und Spezifikation für die Benutzeranmeldung

Unser Backend muss eine grundlegende Anmeldefunktion unterstützen. Lassen Sie uns das Skelett für unsere login_spec , vorausgesetzt, eine gültige Anmeldeanforderung besteht aus einer registrierten E-Mail-Adresse und einem Passwortpaar:

 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

Wenn einer der Parameter fehlt, sollte der Client einen HTTP-Rückgabestatuscode von 400 (dh Bad Request) zusammen mit einer Fehlermeldung „E-Mail fehlt“ oder „Passwort fehlt“ erhalten.

Für unseren Test erstellen wir einen gültigen Benutzer und legen die E-Mail-Adresse und das Passwort des Benutzers als ursprüngliche Parameter für diese Testsuite fest. Dann passen wir diesen Parameter-Hash für jede spezifische Spezifikation an, indem wir entweder das Passwort/die E-Mail weglassen oder es überschreiben.

Lassen Sie uns den Benutzer und den Parameter-Hash am Anfang der Spezifikation erstellen. Wir werden diesen Code nach dem Beschreibungsblock einfügen:

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

Wir können dann unseren 'missing params'/'password'-Kontext wie folgt erweitern:

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

Aber anstatt die Erwartungen über die „E-Mail“- und „Passwort“-Kontexte hinweg zu wiederholen, können wir die gleichen gemeinsamen Beispiele als Erwartungen verwenden. Dazu müssen wir diese Zeile in unserer Datei rails_helper.rb :

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

Wir müssen dann die 3 freigegebenen RSpec-Beispiele 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

Diese freigegebenen Beispiele rufen die api_call Methode auf, die es uns ermöglicht, den API-Endpunkt nur einmal in unserer Spezifikation zu definieren (in Übereinstimmung mit dem DRY-Prinzip). Wir definieren diese Methode wie folgt:

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

Wir müssen auch die Fabrik für unseren Benutzer anpassen:

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

Und schließlich, bevor wir unsere Spezifikationen ausführen, müssen wir unsere Migrationen ausführen:

 rake db:migrate

Beachten Sie jedoch, dass die Spezifikationen an dieser Stelle noch fehlschlagen, da wir unseren API-Endpunkt noch nicht implementiert haben. Das kommt als nächstes.

Implementieren des Anmelde-API-Endpunkts

Für den Anfang schreiben wir ein leeres Skelett für unsere Anmelde-API ( app/api/login.rb ):

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

Als Nächstes schreiben wir eine Aggregatorklasse, die die API-Endpunkte aggregiert ( app/api/api.rb ):

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

OK, jetzt können wir unsere API in den Routen mounten:

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

Lassen Sie uns nun den Code hinzufügen, um nach den fehlenden Parametern zu suchen. Wir können diesen Code zu api.rb hinzufügen, indem wir ihn aus Grape::Exceptions::ValidationErrors retten.

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

Für das ungültige Passwort prüfen wir, ob der HTTP-Antwortcode 401 lautet, was einen unbefugten Zugriff bedeutet. Fügen wir dies dem Kontext „Falsches Passwort“ hinzu:

 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'

Dieselbe Logik wird dann auch dem Kontext „mit nicht vorhandenem Login“ hinzugefügt.

Wir implementieren dann die Logik, die die ungültigen Authentifizierungsversuche behandelt, wie folgt in unsere login.rb :

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

An diesem Punkt werden sich alle negativen Spezifikationen für die Anmelde-API ordnungsgemäß verhalten, aber wir müssen die positiven Spezifikationen für unsere Anmelde-API noch unterstützen. Unsere positive Spezifikation erwartet, dass der Endpunkt einen HTTP-Antwortcode von 200 (dh Erfolg) mit gültigem JSON und einem gültigen Token zurückgibt:

 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

Lassen Sie uns auch die Erwartung für den Antwortcode 200 zu spec/support/shared.rb :

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

Im Falle einer erfolgreichen Anmeldung werden wir das erste gültige Authentifizierungstoken zusammen mit der E-Mail des Benutzers in diesem Format zurückgeben:

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

Wenn es noch kein solches Token gibt, erstellen wir eines für den aktuellen Benutzer:

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

Damit dies funktioniert, benötigen wir eine AuthenticationToken -Klasse, die dem Benutzer gehört. Wir werden dieses Modell generieren und dann die entsprechende Migration ausführen:

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

Wir müssen auch die entsprechende Assoziation zu unserem Benutzermodell hinzufügen:

 class User < ActiveRecord::Base has_many :authentication_tokens end

Dann fügen wir der AuthenticationToken -Klasse den gültigen Bereich hinzu:

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

Beachten Sie, dass wir in der where -Anweisung Ruby-Syntax verwendet haben. Dies wird durch unsere Verwendung des squeel ermöglicht, das die Unterstützung der Ruby-Syntax in ActiveRecord-Abfragen ermöglicht.

Für einen validierten Benutzer werden wir eine Entität erstellen, die wir als „Benutzer mit Token-Entität“ bezeichnen und die Funktionen des grape-entity Gems nutzen.

Lassen Sie uns die Spezifikation für unsere Entität schreiben und in die Datei 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

Als nächstes fügen wir die Entitäten zu user_entity.rb :

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

Fügen Sie schließlich eine weitere Klasse zu user_with_token_entity.rb :

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

Da wir nicht möchten, dass Token unbegrenzt gültig bleiben, lassen wir sie nach einem Tag verfallen:

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

Nachdem dies alles erledigt ist, können wir nun das erwartete JSON-Format mit unserem neu geschriebenen 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 ...

Cool. Jetzt sind alle unsere Spezifikationen erfolgreich und die funktionalen Anforderungen des grundlegenden Anmelde-API-Endpunkts werden unterstützt.

Pair Programming Session Review API-Endpunkt: Erste Schritte

Unser Back-End muss autorisierten Entwicklern, die sich angemeldet haben, erlauben, Sitzungsüberprüfungen für die Paarprogrammierung abzufragen.

Unser neuer API-Endpunkt wird in /api/pair_programming_session und gibt die zu einem Projekt gehörenden Reviews zurück. Beginnen wir damit, ein Grundgerüst für diese Spezifikation zu schreiben:

 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

Wir werden auch einen entsprechenden leeren API-Endpunkt ( app/api/pair_programming_sessions.rb ) schreiben:

 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

Dann mounten wir unsere neue API ( app/api/api.rb ):

 ... mount Login mount PairProgrammingSessions end

Lassen Sie uns die Spezifikation und den API-Endpunkt nacheinander anhand der Anforderungen erweitern.

Pair Programming Session Review API-Endpunkt: Validierung

Eine unserer wichtigsten nicht funktionalen Sicherheitsanforderungen bestand darin, den API-Zugriff auf eine kleine Untergruppe von Entwicklern zu beschränken, die wir verfolgen, also lassen Sie uns Folgendes spezifizieren:

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

Dann erstellen wir ein shared_example in unserer shared.rb , um zu bestätigen, dass die Anfrage von einem unserer registrierten Entwickler kommt:

 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

Wir müssen auch eine ErrorCodes -Klasse erstellen (in app/models/error_codes.rb ):

 module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end

Da wir davon ausgehen, dass unsere API in Zukunft erweitert wird, werden wir einen authorization_helper implementieren, der über alle API-Endpunkte in der Anwendung hinweg wiederverwendet werden kann, um den Zugriff nur auf registrierte Entwickler zu beschränken:

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

Wir werden die Methode „ restrict_access_to_developers “ im Modul „ ApiHelpers::AuthenticationHerlper “ ( app/api/api_helpers/authentication_helper.rb ) definieren. Diese Methode prüft einfach, ob der Schlüssel Authorization unter den Headern einen gültigen ApiKey enthält. (Jeder Entwickler, der auf die API zugreifen möchte, benötigt einen gültigen ApiKey . Dieser kann entweder von einem Systemadministrator oder über einen automatisierten Registrierungsprozess bereitgestellt werden, aber dieser Mechanismus würde den Rahmen dieses Artikels sprengen.)

 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

Anschließend müssen wir das ApiKey-Modell generieren und die Migrationen ausführen: rails g model api_key token rake db:migrate

Nachdem dies erledigt ist, können wir in unserer spec/api/pair_programming_spec.rb überprüfen, ob der Benutzer authentifiziert ist:

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

Lassen Sie uns auch ein unauthenticated freigegebenes Beispiel definieren, das für alle Spezifikationen wiederverwendet werden kann ( 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

Dieses freigegebene Beispiel benötigt das Token im Developer-Header, also fügen wir es unserer Spezifikation hinzu ( spec/api/pair_programming_spec.rb ):

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

Versuchen wir nun in unserer app/api/pair_programming_session.rb , den Benutzer zu authentifizieren:

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

Lassen Sie uns die authenticate! Methode im 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 ...

(Beachten Sie, dass wir den Fehlercode BAD_AUTHENTICATION_PARAMS zu unserer ErrorCodes -Klasse hinzufügen müssen.)

Lassen Sie uns als Nächstes spezifizieren, was passiert, wenn der Entwickler die API mit einem ungültigen Token aufruft. In diesem Fall lautet der Rückgabecode 401, was einen „nicht autorisierten Zugriff“ signalisiert. Das Ergebnis sollte JSON sein und ein Auditable erstellt werden. Also fügen wir dies zu 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 ...

Wir werden die freigegebenen Beispiele „überprüfbar erstellt“, „enthält Fehlercode“ und „enthält Fehlernachricht“ zu 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 ...

Wir müssen auch ein audit_log-Modell erstellen:

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

Pair Programming Session Review API-Endpunkt: Rückgabe von Ergebnissen

Für einen authentifizierten und autorisierten Benutzer sollte ein Aufruf an diesen API-Endpunkt eine Reihe von Paar-Programmierungssitzungsüberprüfungen zurückgeben, die nach Projekt gruppiert sind. Ändern wir unsere spec/api/pair_programming_spec.rb entsprechend:

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

Dies gibt an, dass eine mit gültigem api_key und gültigen Parametern gesendete Anfrage einen HTTP-Code von 200 (dh Erfolg) zurückgibt und dass das Ergebnis in Form von gültigem JSON zurückgegeben wird.

Wir werden diese Paarprogrammierungssitzungen abfragen und dann im JSON-Format zurückgeben, bei denen einer der Teilnehmer der aktuelle Benutzer ist ( 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 ...

Die Pair Programming Sessions sind in der Datenbank wie folgt modelliert:

  • 1-zu-viele-Beziehung zwischen Projekten und Pair-Programming-Sitzungen
  • 1-zu-viele-Beziehung zwischen Pair-Programming-Sitzungen und Reviews
  • 1-zu-viele-Beziehung zwischen Reviews und Codebeispielen

Lassen Sie uns die Modelle entsprechend generieren und dann die Migrationen ausführen:

 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

Dann müssen wir unsere PairProgrammingSession und Review -Klassen ändern, um die has_many Assoziationen zu enthalten:

 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

HINWEIS: Unter normalen Umständen würde ich diese Klassen generieren, indem ich zuerst Spezifikationen für sie schreibe, aber da dies den Rahmen dieses Artikels sprengen würde, überspringe ich diesen Schritt.

Jetzt müssen wir die Klassen schreiben, die unsere Modelle in ihre JSON-Darstellungen umwandeln (in der Grape-Terminologie als Grape-Entities bezeichnet). Der Einfachheit halber verwenden wir eine 1-zu-1-Zuordnung zwischen den Modellen und Traubenentitäten.

Wir beginnen damit, das code aus CodeSampleEntity (in api/entities/code_sample_entity.rb ) verfügbar zu machen:

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

Dann legen wir den user und die zugehörigen code_samples , indem wir die bereits definierte UserEntity und die CodeSampleEntity :

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

Wir legen auch das name aus ProjectEntity :

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

Schließlich fügen wir die Entität zu einer neuen PairProgrammingSessionsEntity zusammen, in der wir das project , den host_user , den visitor_user und die reviews verfügbar machen:

 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

Und damit ist unsere API vollständig implementiert!

Generieren von Testdaten

Zu Testzwecken erstellen wir einige Beispieldaten in db/seeds.rb . Diese Datei sollte die gesamte Datensatzerstellung enthalten, die zum Seeding der Datenbank mit ihren Standardwerten erforderlich ist. Die Daten können dann mit rake db:seed geladen werden (oder mit der db erstellt werden, wenn db:setup aufgerufen wird). Hier ist ein Beispiel dafür, was dies beinhalten könnte:

 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'

Jetzt ist unsere Anwendung einsatzbereit und wir können unseren Rails-Server starten.

Testen der API

Wir werden Swagger verwenden, um einige manuelle browserbasierte Tests unserer API durchzuführen. Es sind jedoch einige Einrichtungsschritte erforderlich, damit wir Swagger nutzen können.

Zuerst müssen wir unserer Gemfile ein paar Edelsteine ​​​​hinzufügen:

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

Wir führen dann bundle aus, um diese Edelsteine ​​zu installieren.

Wir müssen diese auch zu Assets zu unserer Asset-Pipeline hinzufügen (in config/initializers/assets.rb ):

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

Schließlich müssen wir in app/api/api.rb den Swagger-Generator mounten:

 ... add_swagger_documentation end ...

Jetzt können wir die nette Benutzeroberfläche von Swagger nutzen, um unsere API zu erkunden, indem wir einfach zu http://localhost:3000/api/swagger gehen.

Swagger präsentiert unsere API-Endpunkte auf gut erforschbare Weise. Wenn wir auf einen Endpunkt klicken, listet Swagger seine Operationen auf. Wenn wir auf eine Operation klicken, zeigt Swagger ihre erforderlichen und optionalen Parameter und ihre Datentypen an.

Ein letztes Detail, bevor wir fortfahren: Da wir die Verwendung der API-Entwickler mit einem gültigen api_key , können wir nicht direkt vom Browser auf den API-Endpunkt zugreifen, da der Server einen gültigen api_key im HTTP-Header benötigt. Wir können dies zu Testzwecken in Google Chrome erreichen, indem wir das Modify Headers for Google Chrome-Plugin verwenden. Dieses Plugin ermöglicht es uns, den HTTP-Header zu bearbeiten und einen gültigen api_key (wir verwenden den Dummy api_key von 12345654321 , den wir in unsere Datenbank-Seed-Datei aufgenommen haben).

OK, jetzt sind wir bereit zum Testen!

Um den API-Endpunkt pair_programming_sessions , müssen wir uns zuerst anmelden. Wir verwenden einfach die E-Mail- und Passwortkombinationen aus unserer Datenbank-Seed-Datei und senden sie über Swagger an den Anmelde-API-Endpunkt, wie unten gezeigt.

Wie Sie oben sehen können, wird das Token dieses Benutzers zurückgegeben, was darauf hinweist, dass die Anmelde-API so funktioniert, wie es beabsichtigt ist. Wir können dieses Token jetzt verwenden, um die Operation GET /api/pair_programming_sessions.json erfolgreich auszuführen.

Wie gezeigt, wird das Ergebnis als ordnungsgemäß formatiertes hierarchisches JSON-Objekt zurückgegeben. Beachten Sie, dass die JSON-Struktur zwei verschachtelte 1-zu-viele-Zuordnungen widerspiegelt, da das Projekt mehrere Überprüfungen hat und eine Überprüfung mehrere Codebeispiele hat. Wenn wir die Struktur nicht auf diese Weise zurückgeben würden, müsste der Aufrufer unserer API die Überprüfungen für jedes Projekt separat anfordern, was die Übermittlung von N Abfragen an unseren API-Endpunkt erfordern würde. Mit dieser Struktur lösen wir daher das Problem der N+1-Abfrageleistung.

Einpacken

Wie hier gezeigt, tragen umfassende Spezifikationen für Ihre API dazu bei, dass die implementierte API die beabsichtigten (und unbeabsichtigten!) Anwendungsfälle richtig und angemessen adressiert.

Während die in diesem Tutorial vorgestellte Beispiel-API ziemlich einfach ist, können der Ansatz und die Techniken, die wir demonstriert haben, als Grundlage für anspruchsvollere APIs beliebiger Komplexität dienen, die das Grape-Gem verwenden. Dieses Tutorial hat hoffentlich gezeigt, dass Grape ein nützliches und flexibles Juwel ist, das die Implementierung einer JSON-API in Ihren Rails-Anwendungen erleichtern kann. Genießen!