なので Ruby OnRails開発者 、JavaScriptを多用するために、APIエンドポイントでアプリケーションを拡張する必要があることがよくあります リッチインターネット クライアントまたはネイティブのiPhoneおよびAndroidアプリ。アプリケーションの唯一の目的がiPhone / Androidアプリを介して提供することである場合もあります。 JSON 火。
このチュートリアルでは、使用方法を示します 葡萄 – Ruby用のRESTのようなAPIマイクロフレームワーク– JSONAPIのRailsでバックエンドサポートを構築します。ブドウは、として実行するように設計されています 取り付け可能なラックエンジン に 補体 私たちのウェブアプリケーション、なし 干渉 彼らと一緒に。
このチュートリアルで焦点を当てるユースケースは、ペアプログラミングセッションをキャプチャして確認できるアプリケーションです。アプリケーション自体はObjectiveCでiOS用に作成され、データを保存および取得するためにバックエンドサービスと通信する必要があります。このチュートリアルでは、JSONAPIをサポートする堅牢で安全なバックエンドサービスの作成に焦点を当てています。
APIは、次のメソッドをサポートします。
注意: ペアプログラミングセッションのレビューをクエリする機能を提供することに加えて、実際のAPIは、データベースに含めるためにペアプログラミングレビューを送信するための機能も提供する必要があります。 APIを介してそれをサポートすることはこのチュートリアルの範囲を超えているため、データベースにペアプログラミングレビューのサンプルセットが入力されていると単純に想定します。
主な技術要件は次のとおりです。
また、アプリケーションは外部クライアントにサービスを提供する必要があるため、セキュリティに注意を払う必要があります。その目的に向けて:
我々は使用するだろう テスト駆動開発(TDD) APIの決定論的動作を保証するためのソフトウェア開発アプローチとして。
テストの目的で使用します RSpec 、RubyOnRailsコミュニティでよく知られているテストフレームワーク。 したがって、この記事では「テスト」ではなく「仕様」を参照します。
に 包括的なテスト方法 「陽性」と「陰性」の両方のテストで構成されます。負の仕様は、たとえば、一部のパラメーターが欠落しているか正しくない場合にAPIエンドポイントがどのように動作するかを指定します。ポジティブスペックは、APIが正しく呼び出される場合をカバーします。
バックエンドAPIの基盤を築きましょう。まず、新しいRailsアプリケーションを作成する必要があります。
rails new toptal_grape_blog
次に、rspec-rails
を追加してRSpecをインストールしますgemfileに:
group :development, :test do gem 'rspec-rails', '~> 3.2' end
次に、コマンドラインから次のコマンドを実行する必要があります。
rails generate rspec:install
また、テストフレームワークに既存のオープンソースソフトウェアを活用することもできます。具体的には:
ステップ1: これらをgemfileに追加します。
... gem 'devise' ... group :development, :test do ... gem 'factory_girl_rails', '~> 4.5' ... end
ステップ2: ユーザーモデルを生成し、devise
を初期化しますgemを作成し、それをユーザーモデルに追加します(これにより、ユーザークラスを認証に使用できるようになります)。
rails g model user rails generate devise:install rails generate devise user
ステップ3: factory_girl
を含めますrails_helper.rb
の構文メソッド仕様でユーザー作成の短縮バージョンを使用するためのファイル:
RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods
ステップ4: ブドウの宝石をDSLに追加し、インストールします。
gem 'grape' bundle
バックエンドは、基本的なログイン機能をサポートする必要があります。有効なログインリクエストが登録済みのメールアドレスとパスワードのペアで構成されていると仮定して、login_spec
のスケルトンを作成しましょう。
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
いずれかのパラメータが欠落している場合、クライアントは「電子メールが欠落しています」または「パスワードが欠落しています」というエラーメッセージとともに、HTTPリターンステータスコード400(つまり、不正な要求)を受け取る必要があります。
このテストでは、有効なユーザーを作成し、ユーザーのメールアドレスとパスワードをこのテストスイートの元のパラメーターとして設定します。次に、パスワード/電子メールを省略するか、オーバーライドすることにより、特定の仕様ごとにこのパラメーターハッシュをカスタマイズします。
仕様の最初にユーザーとパラメータハッシュを作成しましょう。このコードをdescribeブロックの後に配置します。
describe '/api/login' do let(:email) { user.email } let(:password) { user.password } let!(:user) { create :user } let(:original_params) { { email: email, password: password } } let(:params) { original_params } ...
次に、「不足しているパラメータ」/「パスワード」のコンテキストを次のように拡張できます。
let(:params) { original_params.except(:password) } it_behaves_like '400' it_behaves_like 'json result' it_behaves_like 'contains error msg', 'password is missing'
ただし、「メール」と「パスワード」のコンテキスト全体で期待を繰り返す代わりに、期待と同じ共有例を使用できます。このため、rails_helper.rb
でこの行のコメントを解除する必要がありますファイル:
Dir[Rails.root.join('spec/support/**/*.rb')].each
次に、3つのRSpec共有例を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
これらの共有例はapi_call
を呼び出しています仕様でAPIエンドポイントを1回だけ定義できるようにするメソッド( DRYの原則 )。このメソッドを次のように定義します。
describe '/api/login' do ... def api_call *params post '/api/login', *params end ...
また、ユーザー向けにファクトリをカスタマイズする必要があります。
FactoryGirl.define do factory :user do password 'Passw0rd' password_confirmation sequence(:email) { |n| 'test#{n}@example.com' } end end
そして最後に、仕様を実行する前に、移行を実行する必要があります。
rake db:migrate
ただし、APIエンドポイントをまだ実装していないため、この時点ではまだ仕様が失敗することに注意してください。次です。
手始めに、ログインAPIの空のスケルトンを記述します(app/api/login.rb
):
class Login 次に、APIエンドポイント(app/api/api.rb
)を集約するアグリゲータークラスを記述します。
class API OK、これでAPIをルートにマウントできます。
Rails.application.routes.draw do ... mount API => '/' ... end
次に、不足しているパラメーターをチェックするコードを追加しましょう。そのコードをapi.rb
に追加できますGrape::Exceptions::ValidationErrors
から救助することによって。
rescue_from Grape::Exceptions::ValidationErrors do |e| rack_response({ status: e.status, error_msg: e.message, }.to_json, 400) end
無効なパスワードについては、http応答コードが不正アクセスを意味する401であるかどうかを確認します。これを「不正なパスワード」コンテキストに追加しましょう。
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'
次に、同じロジックが「ログインが存在しない」コンテキストにも追加されます。
次に、無効な認証の試行を処理するロジックを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
この時点で、ログインAPIのすべてのネガティブスペックは適切に動作しますが、ログインAPIのポジティブスペックをサポートする必要があります。私たちの肯定的な仕様では、エンドポイントが有効なJSONと有効なトークンを含む200のHTTP応答コード(つまり、成功)を返すことを期待しています。
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
また、応答コード200の期待値をspec/support/shared.rb
に追加しましょう。
RSpec.shared_examples '200' do specify 'returns 200' do api_call params expect(response.status).to eq(200) end end
ログインに成功した場合、最初の有効なauthentication_tokenをユーザーのメールアドレスと一緒に次の形式で返します。
{‘email’:, ‘token’:}
そのようなトークンがまだない場合は、現在のユーザー用にトークンを作成します。
... if user.present? && user.valid_password?(params[:password]) token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user) status 200 else ...
これが機能するためには、AuthenticationToken
が必要です。ユーザーに属するクラス。このモデルを生成してから、対応する移行を実行します。
rails g model authentication_token token user:references expires_at:datetime rake db:migrate
また、対応する関連付けをユーザーモデルに追加する必要があります。
class User 次に、有効なスコープをAuthenticationToken
に追加しますクラス:
class AuthenticationToken { where (expires_at > Time.zone.now) } end
where
でRuby構文を使用したことに注意してくださいステートメント。これは、 squeel
宝石 これにより、activerecordクエリでのRuby構文のサポートが可能になります。
検証済みのユーザーの場合、「トークンを持つユーザーエンティティ」と呼ばれるエンティティを作成し、の機能を活用します。 grape-entity
宝石 。
エンティティの仕様を記述して、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
次に、エンティティをuser_entity.rb
に追加しましょう。
module Entities class UserEntity そして最後に、user_with_token_entity.rb
に別のクラスを追加します。
module Entities class UserWithTokenEntity トークンが無期限に有効であり続けることを望まないため、トークンは1日後に期限切れになります。
FactoryGirl.define do factory :authentication_token do token 'MyString' expires_at 1.day.from_now user end end
これがすべて完了したら、新しく記述したUserWithTokenEntity
を使用して期待されるJSON形式を返すことができます。
... 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 ...
涼しい。これですべての仕様に合格し、基本的なログインAPIエンドポイントの機能要件がサポートされました。
ペアプログラミングセッションレビューAPIエンドポイント:はじめに
私たちのバックエンドは許可する必要があります 承認済み ペアプログラミングセッションのレビューをクエリするためにログインした開発者。
新しいAPIエンドポイントは/api/pair_programming_session
にマウントされますプロジェクトに属するレビューを返します。この仕様の基本的なスケルトンを作成することから始めましょう。
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
対応する空のAPIエンドポイント(app/api/pair_programming_sessions.rb
)も記述します。
class PairProgrammingSessions 次に、新しいAPIをマウントしましょう(app/api/api.rb
):
... mount Login mount PairProgrammingSessions end
要件に合わせて、仕様とAPIエンドポイントを1つずつ拡張してみましょう。
ペアプログラミングセッションレビューAPIエンドポイント:検証
最も重要な非機能セキュリティ要件の1つは、追跡する開発者の小さなサブセットにAPIアクセスを制限することでした。そのため、次のように指定しましょう。
... 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 ...
次に、shared_example
を作成します私たちのshared.rb
リクエストが登録済みの開発者の1人からのものであることを確認するには:
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
また、ErrorCodes
を作成する必要がありますクラス(app/models/error_codes.rb
内):
module ErrorCodes DEVELOPER_KEY_MISSING = 1001 end
APIは将来拡張されると予想されるため、authorization_helper
を実装します。これは、アプリケーション内のすべてのAPIエンドポイントで再利用して、登録済みの開発者のみにアクセスを制限できます。
class PairProgrammingSessions メソッドを定義しますrestrict_access_to_developers
ApiHelpers::AuthenticationHerlper
でモジュール(app/api/api_helpers/authentication_helper.rb
)。このメソッドは、キーAuthorization
かどうかをチェックするだけです。ヘッダーの下には、有効なApiKey
が含まれています。 (APIへのアクセスを希望するすべての開発者は、有効なApiKey
を必要とします。これは、システム管理者または自動登録プロセスを介して提供できますが、そのメカニズムはこの記事の範囲を超えています。)
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} With 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
次に、ApiKeyモデルを生成し、移行を実行する必要があります。railsg model api_key token rake db:migrate
これが完了すると、spec/api/pair_programming_spec.rb
でユーザーが認証されているかどうかを確認できます。
... it_behaves_like 'restricted for developers' it_behaves_like 'unauthenticated' ...
unauthenticated
も定義しましょうすべての仕様で再利用できる共有例(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
この共有例では、開発者ヘッダーにトークンが必要なので、それを仕様に追加しましょう(spec/api/pair_programming_spec.rb
):
... describe '/api' do let(:api_key) { create :api_key } let(:developer_header) { {'Authorization' => api_key.token} } ...
それでは、app/api/pair_programming_session.rb
で、ユーザーの認証を試みましょう。
... class PairProgrammingSessions authenticate!
を実装しましょう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 ...
(エラーコードBAD_AUTHENTICATION_PARAMS
をErrorCodes
クラスに追加する必要があることに注意してください。)
次に、開発者が無効なトークンを使用してAPIを呼び出した場合にどうなるかを特定しましょう。その場合、リターンコードは401になり、「不正アクセス」を通知します。結果はJSONになり、監査可能なものが作成されます。したがって、これを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 ...
「auditablecreated」、「contains error code」、「containserrormsg」の共有例を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 ...
また、audit_logモデルを作成する必要があります。
rails g model audit_log backtrace data user:references rake db:migrate
ペアプログラミングセッションレビューAPIエンドポイント:結果を返す
認証および承認されたユーザーの場合、このAPIエンドポイントを呼び出すと、プロジェクトごとにグループ化されたペアプログラミングセッションレビューのセットが返されます。 spec/api/pair_programming_spec.rb
を変更しましょうしたがって:
... context 'valid params' do it_behaves_like '200' it_behaves_like 'json result' end ...
これは、有効なapi_key
で送信されたリクエストを指定します有効なパラメーターは200のHTTPコード(つまり成功)を返し、結果は有効なJSONの形式で返されます。
クエリを実行してから、参加者のいずれかが現在のユーザー(app/api/pair_programming_sessions.rb
)であるペアプログラミングセッションをJSON形式で返します。
... 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 ...
ペアプログラミングセッションは、データベースで次のようにモデル化されています。
javascriptはミリ秒単位で時間を取得します
- プロジェクトとペアプログラミングセッション間の1対多の関係
- ペアプログラミングセッションとレビューの間の1対多の関係
- レビューとコードサンプル間の1対多の関係
それに応じてモデルを生成してから、移行を実行しましょう。
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
次に、PairProgrammingSession
を変更する必要がありますおよびReview
has_many
を含むクラス協会:
class Review 注意: 通常の状況では、最初に仕様を記述してこれらのクラスを生成しますが、それはこの記事の範囲を超えているため、そのステップはスキップします。
次に、モデルをJSON表現(と呼ばれる)に変換するクラスを作成する必要があります。 ブドウの実体 ブドウの用語で)。簡単にするために、モデルとブドウの実体の間で1対1のマッピングを使用します。
code
を公開することから始めますCodeSampleEntity
からのフィールド(api/entities/code_sample_entity.rb
内):
module Entities class CodeSampleEntity 次に、user
を公開しますおよび関連するcode_samples
すでに定義されているUserEntity
を再利用するおよびCodeSampleEntity
:
module Entities class ReviewEntity name
も公開しますProjectEntity
のフィールド:
module Entities class ProjectEntity 最後に、エンティティを新しいPairProgrammingSessionsEntity
にアセンブルしますここで、project
、host_user
、visitor_user
を公開しますおよびreviews
:
module Entities class PairProgrammingSessionsEntity これで、APIが完全に実装されました。
テストデータの生成
テストの目的で、db/seeds.rb
にいくつかのサンプルデータを作成します。このファイルには、データベースにデフォルト値をシードするために必要なすべてのレコード作成が含まれている必要があります。その後、データはrake db:seed
でロードできます。 (またはdb:setup
が呼び出されたときにdbで作成されます)。これに含まれる可能性のあるものの例を次に示します。
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: 'ApeeScape 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'
これでアプリケーションを使用する準備が整い、Railsサーバーを起動できます。
APIのテスト
我々は使用するだろう Swagger APIの手動ブラウザベースのテストを行うため。ただし、Swaggerを使用できるようにするには、いくつかのセットアップ手順が必要です。
まず、gemfileにいくつかのgemを追加する必要があります。
... gem 'grape-swagger' gem 'grape-swagger-ui' ...
次に、bundle
を実行しますこれらの宝石をインストールします。
また、これらをアセットパイプライン(config/initializers/assets.rb
内)のアセットに追加する必要があります。
Rails.application.config.assets.precompile += %w( swagger_ui.js ) Rails.application.config.assets.precompile += %w( swagger_ui.css )
最後に、app/api/api.rb
でSwaggerジェネレーターをマウントする必要があります。
... add_swagger_documentation end ...
これで、Swaggerの優れたUIを利用して、http://localhost:3000/api/swagger
に移動するだけでAPIを探索できます。
Swaggerは、APIエンドポイントをうまく探索可能な方法で提示します。エンドポイントをクリックすると、Swaggerはその操作を一覧表示します。操作をクリックすると、Swaggerに必須およびオプションのパラメーターとそのデータ型が表示されます。
先に進む前に、残りの1つの詳細: 有効なapi_key
でAPI開発者の使用を制限したため、サーバーには有効なapi_key
が必要になるため、ブラウザーからAPIエンドポイントに直接アクセスすることはできません。 HTTPヘッダー内。これは、Google Chromeでのテスト目的で、 GoogleChromeプラグインのヘッダーを変更する 。このプラグインを使用すると、HTTPヘッダーを編集して、有効なapi_key
を追加できます。 (データベースシードファイルに含めたapi_key
のダミー12345654321
を使用します)。
OK、これでテストの準備が整いました。
pair_programming_sessions
を呼び出すためにAPIエンドポイント、最初にログインする必要があります。以下に示すように、データベースシードファイルからのメールとパスワードの組み合わせを使用して、Swaggerを介してログインAPIエンドポイントに送信します。

上記のように、そのユーザーに属するトークンが返され、ログインAPIが意図したとおりに正しく機能していることを示します。これで、そのトークンを使用してGET /api/pair_programming_sessions.json
を正常に実行できます。操作。

示されているように、結果は適切にフォーマットされた階層JSONオブジェクトとして返されます。プロジェクトには複数のレビューがあり、レビューには複数のコードサンプルがあるため、JSON構造は2つのネストされた1対多の関連付けを反映していることに注意してください。この方法で構造を返さない場合、APIの呼び出し元は、プロジェクトごとに個別にレビューをリクエストする必要があり、APIエンドポイントにN個のクエリを送信する必要があります。したがって、この構造を使用して、N +1クエリのパフォーマンスの問題を解決します。
要約
ここに示すように、APIの包括的な仕様は、実装されたAPIが意図された(そして意図されていない!)ユースケースに適切かつ適切に対処することを保証するのに役立ちます。
このチュートリアルで紹介するAPIの例はかなり基本的なものですが、ここで示したアプローチと手法は、を使用して任意の複雑さのより洗練されたAPIの基盤として機能します。 葡萄 宝石。このチュートリアルは、GrapeがRailsアプリケーションでのJSONAPIの実装を容易にするのに役立つ便利で柔軟なgemであることを示していると思います。楽しい!