บทช่วยสอน Grape Gem: วิธีสร้าง API ที่เหมือนพักผ่อนใน Ruby
เผยแพร่แล้ว: 2022-03-11ในฐานะนักพัฒนา Ruby On Rails เรามักจะต้องขยายแอปพลิเคชันของเราด้วยตำแหน่งข้อมูล API เพื่อรองรับไคลเอนต์ Rich Internet ที่ใช้งาน JavaScript จำนวนมาก หรือแอปสำหรับ iPhone และ Android ที่มาพร้อมเครื่อง นอกจากนี้ยังมีบางกรณีที่วัตถุประสงค์เพียงอย่างเดียวของแอปพลิเคชันคือเพื่อให้บริการแอป iPhone/Android ผ่าน JSON API
ในบทช่วยสอนนี้ ฉันสาธิตวิธีใช้ Grape ซึ่งเป็นไมโครเฟรมเวิร์ก API ที่เหมือน REST สำหรับ Ruby เพื่อสร้างการรองรับแบ็กเอนด์ใน Rails สำหรับ JSON API Grape ได้รับการออกแบบมาให้ทำงานเป็นกลไกแร็คแบบติดตั้งได้เพื่อ เสริม เว็บแอปพลิเคชันของเรา โดยไม่ รบกวนการทำงาน เหล่านี้
ใช้กรณี
กรณีการใช้งานที่เราจะเน้นในบทช่วยสอนนี้คือแอปพลิเคชันที่สามารถบันทึกและตรวจสอบเซสชันการเขียนโปรแกรมคู่ แอปพลิเคชันเองจะถูกเขียนขึ้นสำหรับ iOS ใน ObjectiveC และจะต้องสื่อสารกับบริการแบ็กเอนด์เพื่อจัดเก็บและเรียกข้อมูล จุดเน้นของเราในบทช่วยสอนนี้คือการสร้างบริการแบ็กเอนด์ที่แข็งแกร่งและปลอดภัยซึ่งรองรับ JSON API
API จะสนับสนุนวิธีการสำหรับ:
- เข้าสู่ระบบ
- สอบถามความคิดเห็นเซสชั่นการเขียนโปรแกรมคู่
หมายเหตุ: นอกเหนือจากการให้ความสามารถในการสืบค้นการตรวจสอบเซสชันการเขียนโปรแกรมคู่แล้ว API จริงยังต้องจัดเตรียมสิ่งอำนวยความสะดวกสำหรับการส่งการตรวจสอบการเขียนโปรแกรมคู่เพื่อรวมไว้ในฐานข้อมูล เนื่องจากการสนับสนุนผ่าน API นั้นอยู่นอกเหนือขอบเขตของบทช่วยสอนนี้ เราเพียงแค่สันนิษฐานว่าฐานข้อมูลได้รับการเติมด้วยชุดตัวอย่างการตรวจสอบการเขียนโปรแกรมคู่
ข้อกำหนดทางเทคนิคที่สำคัญ ได้แก่ :
- การเรียก API ทุกครั้งจะต้องส่งคืน JSON . ที่ถูกต้อง
- ทุกการเรียก API ที่ล้มเหลวจะต้องถูกบันทึกด้วยบริบทและข้อมูลที่เพียงพอเพื่อให้ทำซ้ำได้ในภายหลัง และแก้จุดบกพร่องหากจำเป็น
นอกจากนี้ เนื่องจากแอปพลิเคชันของเราจะต้องให้บริการลูกค้าภายนอก เราจึงต้องคำนึงถึงความปลอดภัยด้วย ไปยังจุดสิ้นสุดนั้น:
- คำขอแต่ละรายการควรจำกัดไว้เฉพาะนักพัฒนากลุ่มเล็กๆ ที่เราติดตาม
- คำขอทั้งหมด (นอกเหนือจากการเข้าสู่ระบบ/ลงทะเบียน) จะต้องได้รับการตรวจสอบสิทธิ์
การพัฒนาที่ขับเคลื่อนด้วยการทดสอบและ RSpec
เราจะใช้ Test Driven Development (TDD) เป็นแนวทางการพัฒนาซอฟต์แวร์ของเรา เพื่อช่วยให้แน่ใจว่าพฤติกรรมที่กำหนดขึ้นของ API ของเรา
เพื่อวัตถุประสงค์ในการทดสอบ เราจะใช้ RSpec ซึ่งเป็นเฟรมเวิร์กการทดสอบที่รู้จักกันดีในชุมชน RubyOnRails ฉันจะอ้างถึงในบทความนี้ถึง "ข้อกำหนด" มากกว่า "การทดสอบ"
วิธีการทดสอบที่ครอบคลุมประกอบด้วยการทดสอบทั้งแบบ "บวก" และ "เชิงลบ" ข้อมูลจำเพาะเชิงลบจะระบุ เช่น วิธีการทำงานของปลายทาง API หากพารามิเตอร์บางตัวขาดหายไปหรือไม่ถูกต้อง ข้อกำหนดเชิงบวกครอบคลุมกรณีที่ API ถูกเรียกใช้อย่างถูกต้อง
เริ่มต้น
มาวางรากฐานสำหรับแบ็กเอนด์ API ของเรากัน ขั้นแรก เราต้องสร้างแอปพลิเคชั่นรางใหม่:
rails new toptal_grape_blog
ต่อไป เราจะติดตั้ง RSpec โดยเพิ่ม rspec-rails
ลงใน gemfile ของเรา:
group :development, :test do gem 'rspec-rails', '~> 3.2' end
จากบรรทัดคำสั่งของเรา เราต้องเรียกใช้:
rails generate rspec:install
เรายังสามารถใช้ซอฟต์แวร์โอเพ่นซอร์สที่มีอยู่สำหรับเฟรมเวิร์กการทดสอบของเราได้อีกด้วย โดยเฉพาะ:
- ประดิษฐ์ - โซลูชันการตรวจสอบสิทธิ์ที่ยืดหยุ่นสำหรับ Rails ตาม Warden
- factory_girl_rails - ให้การรวม Rails สำหรับ factory_girl ซึ่งเป็นไลบรารีสำหรับตั้งค่าวัตถุ Ruby เป็นข้อมูลทดสอบ
ขั้นตอนที่ 1: เพิ่มสิ่งเหล่านี้ลงใน gemfile ของเรา:
... gem 'devise' ... group :development, :test do ... gem 'factory_girl_rails', '~> 4.5' ... end
ขั้นตอนที่ 2: สร้างโมเดลผู้ใช้ เริ่มต้นเจม devise
และเพิ่มไปยังโมเดลผู้ใช้ (ซึ่งจะทำให้คลาสผู้ใช้สามารถใช้สำหรับการตรวจสอบสิทธิ์ได้)
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: เพิ่ม Grape gem ลงใน 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 '/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 { |f| require f }
จากนั้นเราต้องเพิ่มตัวอย่างที่แชร์ 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 ได้เพียงครั้งเดียวในข้อมูลจำเพาะของเรา (ตามหลักการ DRY) เรากำหนดวิธีนี้ดังนี้:
describe '/api/login' do ... def api_call *params post "/api/login", *params end ...
เรายังต้องปรับแต่งโรงงานสำหรับผู้ใช้ของเรา:
FactoryGirl.define do factory :user do password "Passw0rd" password_confirmation { |u| u.password } sequence(:email) { |n| "test#{n}@example.com" } end end
และสุดท้าย ก่อนเรียกใช้ข้อมูลจำเพาะ เราต้องเรียกใช้การย้ายข้อมูล:
rake db:migrate
อย่างไรก็ตาม โปรดจำไว้ว่าข้อมูลจำเพาะจะยังคงล้มเหลว ณ จุดนี้ เนื่องจากเรายังไม่ได้ใช้งานปลายทาง API ของเรา ถัดไป
การปรับใช้ปลายทาง API การเข้าสู่ระบบ
สำหรับผู้เริ่มต้น เราจะเขียนโครงร่างว่างสำหรับ 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
ต่อไป เราจะเขียนคลาสตัวรวบรวมซึ่งรวมจุดปลาย API ( app/api/api.rb
):
class API < Grape::API prefix 'api' mount Login end
ตกลง ตอนนี้เราสามารถเมานต์ 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 การเข้าสู่ระบบของเรา ข้อมูลจำเพาะเชิงบวกของเราคาดว่าปลายทางจะส่งคืนรหัสตอบกลับ HTTP 200 (เช่น สำเร็จ) พร้อม JSON ที่ถูกต้องและโทเค็นที่ถูกต้อง:
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':<the_email_of_the_user>, 'token':<the users first valid 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 < ActiveRecord::Base has_many :authentication_tokens end
จากนั้นเราจะเพิ่มขอบเขตที่ถูกต้องให้กับคลาส AuthenticationToken
:
class AuthenticationToken < ActiveRecord::Base belongs_to :user validates :token, presence: true scope :valid, -> { where{ (expires_at == nil) | (expires_at > Time.zone.now) } } end
โปรดทราบว่าเราใช้ไวยากรณ์ของ Ruby ในคำสั่ง where
สิ่งนี้เปิดใช้งานโดยการใช้ squeel
gem ซึ่งช่วยให้รองรับไวยากรณ์ Ruby ในการสืบค้น activerecord
สำหรับผู้ใช้ที่ตรวจสอบแล้ว เราจะสร้างเอนทิตีที่เราจะเรียกว่า "ผู้ใช้ที่มีเอนทิตีโทเค็น" โดยใช้ประโยชน์จากคุณสมบัติของอัญมณี 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 < Grape::Entity expose :email end end
และสุดท้าย เพิ่มคลาสอื่นให้กับ user_with_token_entity.rb
:
module Entities class UserWithTokenEntity < UserEntity expose :token do |user, options| user.authentication_tokens.valid.first.token end end end
เนื่องจากเราไม่ต้องการให้โทเค็นยังคงใช้ได้โดยไม่มีกำหนด เราจึงกำหนดให้โทเค็นหมดอายุหลังจากหนึ่งวัน:
FactoryGirl.define do factory :authentication_token do token "MyString" expires_at 1.day.from_now user end end
เมื่อเสร็จแล้ว เราสามารถส่งคืนรูปแบบ JSON ที่คาดหวังด้วย 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 ...
เย็น. ตอนนี้ข้อกำหนดทั้งหมดของเรากำลังผ่านและรองรับข้อกำหนดด้านการทำงานของจุดปลาย 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 < 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
จากนั้นมาเมานต์ api ใหม่ของเรา ( app/api/api.rb
):
... mount Login mount PairProgrammingSessions end
มาขยายข้อมูลจำเพาะและจุดสิ้นสุด API กับข้อกำหนดทีละรายการ
ปลายทาง API การตรวจสอบเซสชันการเขียนโปรแกรมจับคู่: การตรวจสอบความถูกต้อง
ข้อกำหนดด้านความปลอดภัยที่ไม่ทำงานที่สำคัญที่สุดประการหนึ่งของเราคือการจำกัดการเข้าถึง 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
ของเราเพื่อยืนยันว่าคำขอนั้นมาจากหนึ่งในนักพัฒนาซอฟต์แวร์ที่ลงทะเบียนของเรา:

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 < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers }
เรากำลังกำหนดวิธีการ 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}\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
จากนั้นเราต้องสร้างโมเดล ApiKey และเรียกใช้การโยกย้าย: rails g 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 < Grape::API helpers ApiHelpers::AuthenticationHelper before { restrict_access_to_developers } before { authenticate! } ...
มาดำเนินการ 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 ...
เราจะเพิ่มตัวอย่างที่แชร์ "สร้างที่ตรวจสอบได้" "มีรหัสข้อผิดพลาด" และ "มีข้อความแสดงข้อผิดพลาด" ใน 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
ที่ถูกต้องและพารามิเตอร์ที่ถูกต้องส่งคืนรหัส HTTP 200 (เช่น สำเร็จ) และผลลัพธ์จะถูกส่งกลับในรูปแบบ JSON ที่ถูกต้อง
เราจะทำการสอบถามแล้วกลับมาในรูปแบบ JSON ของเซสชันการเขียนโปรแกรมคู่ที่ผู้เข้าร่วมเป็นผู้ใช้ปัจจุบัน ( 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 ...
เซสชั่นการเขียนโปรแกรมคู่ถูกสร้างแบบจำลองดังต่อไปนี้ในฐานข้อมูล:
- ความสัมพันธ์แบบ 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 < 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
หมายเหตุ: ในสถานการณ์ปกติ ฉันจะสร้างคลาสเหล่านี้โดยการเขียนข้อกำหนดสำหรับพวกเขาก่อน แต่เนื่องจากนั่นอยู่นอกเหนือขอบเขตของบทความนี้ ฉันจะข้ามขั้นตอนนั้น
ตอนนี้ เราต้องเขียนคลาสเหล่านั้นซึ่งจะเปลี่ยนโมเดลของเราให้เป็นตัวแทน JSON ของพวกเขา (เรียกว่าเอนทิตีองุ่นในคำศัพท์เกี่ยวกับองุ่น) เพื่อความง่าย เราจะใช้การแมปแบบ 1 ต่อ 1 ระหว่างแบบจำลองและเอนทิตีองุ่น
เราเริ่มต้นด้วยการเปิดเผยฟิลด์ code
จาก CodeSampleEntity
(ใน api/entities/code_sample_entity.rb
):
module Entities class CodeSampleEntity < Grape::Entity expose :code end end
จากนั้นเราเปิดเผย user
และ code_samples
ที่เกี่ยวข้องโดยนำ UserEntity
ที่กำหนดไว้แล้วและ CodeSampleEntity
กลับมาใช้ใหม่:
module Entities class ReviewEntity < Grape::Entity expose :user, using: UserEntity expose :code_samples, using: CodeSampleEntity end end
นอกจากนี้เรายังเปิดเผยฟิลด์ name
จาก ProjectEntity
:
module Entities class ProjectEntity < Grape::Entity expose :name end end
สุดท้าย เราประกอบเอนทิตีเป็น PairProgrammingSessionsEntity
ใหม่ โดยเราจะเปิดเผย project
กต์ , host_user
, visitor_user
และ 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
และด้วยเหตุนี้ API ของเราจึงถูกใช้งานอย่างสมบูรณ์!
กำลังสร้างข้อมูลการทดสอบ
เพื่อวัตถุประสงค์ในการทดสอบ เราจะสร้างข้อมูลตัวอย่างบางส่วนใน db/seeds.rb
ไฟล์นี้ควรมีการสร้างเร็กคอร์ดทั้งหมดที่จำเป็นในการ seed ฐานข้อมูลด้วยค่าดีฟอลต์ ข้อมูลสามารถโหลดด้วย rake db:seed
(หรือสร้างด้วย db เมื่อเรียกใช้ db:setup
) ต่อไปนี้คือตัวอย่างที่อาจรวมถึง:
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'
ตอนนี้แอปพลิเคชันของเราพร้อมใช้งานแล้ว และเราสามารถเปิดเซิร์ฟเวอร์ rails ของเราได้
การทดสอบ API
เราจะใช้ Swagger เพื่อทำการทดสอบ API ของเราโดยใช้เบราว์เซอร์ด้วยตนเอง จำเป็นต้องมีขั้นตอนการตั้งค่าเล็กน้อยเพื่อให้เราสามารถใช้ประโยชน์จาก Swagger ได้
ขั้นแรก เราต้องเพิ่มอัญมณีสองสามอันลงใน gemfile ของเรา:
... 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
เราจำเป็นต้องติดตั้งตัวสร้างกร่าง:
... add_swagger_documentation end ...
ตอนนี้ เราสามารถใช้ประโยชน์จาก UI ที่ดีของ Swagger เพื่อสำรวจ API ของเราได้โดยไปที่ http://localhost:3000/api/swagger
Swagger นำเสนอจุดปลาย API ของเราในลักษณะที่สำรวจได้อย่างดี ถ้าเราคลิกที่จุดปลาย Swagger จะแสดงรายการการทำงาน หากเราคลิกที่การดำเนินการ Swagger จะแสดงพารามิเตอร์ที่จำเป็นและเป็นทางเลือกและประเภทข้อมูล
รายละเอียดที่เหลือก่อนที่เราจะดำเนินการต่อ: เนื่องจากเราจำกัดการใช้นักพัฒนา API ด้วย api_key
ที่ถูกต้อง เราจึงไม่สามารถเข้าถึงปลายทาง API ได้โดยตรงจากเบราว์เซอร์ เนื่องจากเซิร์ฟเวอร์จะต้องมี api_key
ที่ถูกต้องในส่วนหัว HTTP เราสามารถทำได้เพื่อจุดประสงค์ในการทดสอบใน Google Chrome โดยใช้ปลั๊กอิน Modify Headers สำหรับ Google Chrome ปลั๊กอินนี้จะช่วยให้เราแก้ไขส่วนหัว HTTP และเพิ่ม api_key
ที่ถูกต้องได้ (เราจะใช้ api_key
จำลองของ 12345654321
ที่เรารวมไว้ในไฟล์ฐานข้อมูลของเรา)
ตกลง ตอนนี้เราพร้อมที่จะทดสอบแล้ว!
ในการเรียกจุดสิ้นสุดของ pair_programming_sessions
API เราต้องเข้าสู่ระบบก่อน เราจะใช้อีเมลและรหัสผ่านที่รวมกันจากไฟล์ฐานข้อมูลของเราแล้วส่งผ่าน Swagger ไปยังปลายทางการเข้าสู่ระบบ API ดังที่แสดงด้านล่าง
ดังที่คุณเห็นด้านบน โทเค็นที่เป็นของผู้ใช้นั้นจะถูกส่งคืน ซึ่งบ่งชี้ว่า API การเข้าสู่ระบบทำงานอย่างถูกต้องตามที่ตั้งใจไว้ ตอนนี้เราสามารถใช้โทเค็นนั้นเพื่อดำเนินการ GET /api/pair_programming_sessions.json
ได้สำเร็จ
ดังที่แสดง ผลลัพธ์จะถูกส่งกลับเป็นออบเจ็กต์ JSON ที่มีการจัดรูปแบบตามลำดับชั้นอย่างถูกต้อง โปรดสังเกตว่าโครงสร้าง JSON สะท้อนถึงการเชื่อมโยงแบบ 1 ต่อกลุ่มที่ซ้อนกันสองแบบ เนื่องจากโปรเจ็กต์มีการตรวจทานหลายรายการ และการตรวจทานมีตัวอย่างโค้ดหลายรายการ หากเราไม่ส่งคืนโครงสร้างในลักษณะนี้ ผู้เรียก API ของเราจะต้องร้องขอการตรวจสอบแยกต่างหากสำหรับแต่ละโปรเจ็กต์ ซึ่งจะต้องมีการส่งการสืบค้น N ไปยังจุดปลาย API ของเรา ด้วยโครงสร้างนี้ เราจึงแก้ปัญหาประสิทธิภาพการสืบค้น N+1
สรุป
ตามที่แสดงในที่นี้ ข้อกำหนดที่ครอบคลุมสำหรับ API ของคุณช่วยให้มั่นใจว่า API ที่นำไปใช้อย่างเหมาะสมและเพียงพอสามารถจัดการกับกรณีการใช้งานที่ต้องการ (และไม่ได้ตั้งใจ!)
แม้ว่าตัวอย่าง API ที่นำเสนอในบทช่วยสอนนี้ค่อนข้างพื้นฐาน แต่แนวทางและเทคนิคที่เราได้แสดงให้เห็นสามารถใช้เป็นพื้นฐานสำหรับ API ที่ซับซ้อนยิ่งขึ้นของความซับซ้อนตามอำเภอใจโดยใช้ Grape gem บทช่วยสอนนี้แสดงให้เห็นว่า Grape เป็นอัญมณีที่มีประโยชน์และยืดหยุ่น ซึ่งสามารถช่วยอำนวยความสะดวกในการใช้งาน JSON API ในแอปพลิเคชัน Rails ของคุณ สนุก!