æè¡é¨ã®å°é(@taiki45)ã§ãããã®è¨äºã§ã¯ç°¡åãªã¢ããªã±ã¼ã·ã§ã³(ããã°ã·ã¹ãã )ã®å®è£ ãéãã¦ãã¯ãã¯ãããã§ä½æã»ä½¿ç¨ãã¦ããã©ã¤ãã©ãªã®Garage ã®ç´¹ä»ã¨ Garage ã使ã£ã RESTful Web API ã®éçºããç´¹ä»ãããã¨æãã¾ãã
Garage 㯠RESTful Web API ãéçºããããã®ã Rails gemified plugins ã§ããRails ããã°ã©ã㯠Garage ã使ã£ã¦ Rails ãæ¡å¼µãããã¨ã§ç´ æ©ã Web API ãéçºãããã¨ãã§ãã¾ããGarage ã¯æ°ããã¢ããªã±ã¼ã·ã§ã³ãéçºããå ´åã«ããæ¢åã® Rails ã¢ããªã±ã¼ã·ã§ã³ã«çµã¿è¾¼ã㧠Web API ãå®è£ ããå ´åã§ã使ç¨ã§ãã¾ããGarage ã¯ãªã½ã¼ã¹ã®ã·ãªã¢ã©ã¤ãºãã¢ã¯ã»ã¹ã³ã³ããã¼ã«ãªã© Web API ã®å®è£ ã«å¿ è¦ãªæ©è½ãã«ãã¼ãã¦ãã¾ãã
RubyKaigi2014 ã«ã¦ Garage ã® OSS åããç¥ãããã¾ããããå®éã®ã¢ããªã±ã¼ã·ã§ã³éçºåãã®æ å ±ãå°ãªãã®ã§ããã®è¨äºã¨ãµã³ãã«ã¢ããªã±ã¼ã·ã§ã³ãéãã¦è£å®ãããã¨æãã¾ãã
ãã®è¨äºã§å®è£ ããããã°ã¢ããªã±ã¼ã·ã§ã³ã®ã³ã¼ã㯠https://github.com/taiki45/garage-example ã«ããã¾ãã
ä»åå®è£ ããã¢ããªã±ã¼ã·ã§ã³
次ã®ãããªããã°ã·ã¹ãã ãå®è£ ãã¾ãã
- ã¢ããªã±ã¼ã·ã§ã³ãæä¾ãããªã½ã¼ã¹ã¯ãã°ã¤ã³ã¦ã¼ã¶ã¼ã§ãã user ã¨æ稿ãããæ稿ã§ãã post ã®2ã¤ã
- user ã«ã¤ãã¦ä»¥ä¸ã®æä½ãæä¾ãã¾ã
- ã¦ã¼ã¶ã¼ã®ä¸è¦§ã®è¡¨ç¤º
GET /v1/users
- ããããã®ã¦ã¼ã¶ã¼ã®æ
å ±ã®è¡¨ç¤º
GET /v1/users/:user_id
- èªèº«ã®æ
å ±ã®æ´æ°
PUT /v1/users/:user_id
- ã¦ã¼ã¶ã¼ã®ä¸è¦§ã®è¡¨ç¤º
- post ã«ã¤ãã¦ã¯ä»¥ä¸ã®æä½ãæä¾ãã¾ãã
- æ°è¦è¨äºã®ä½æ
POST /v1/posts
- ã¢ããªã±ã¼ã·ã§ã³å
¨ä½ã®è¨äºã®ä¸è¦§ã®è¡¨ç¤º
GET /v1/posts
- ããã¦ã¼ã¶ã¼ã®æ稿ããè¨äºä¸è¦§ã®è¡¨ç¤º
GET /v1/users/:user_id/posts
- ããããã®è¨äºã®æ
å ±ã®è¡¨ç¤º
GET /v1/posts/:post_id
- èªèº«ã®æ稿ããè¨äºã®æ´æ°
PUT /v1/posts/:post_id
- æ稿ããè¨äºã®åé¤
DELETE /v1/posts/:post_id
- æ°è¦è¨äºã®ä½æ
- user ã®ä½æãåé¤ã«ã¤ãã¦ã¯å®è£ ãã¾ããã
å®éã®éçºã ã¨ã¯ã©ã¤ã¢ã³ãã¢ããªã±ã¼ã·ã§ã³ã® API å©ç¨ã®ä»æ¹ã«ãã£ã¦ããªã½ã¼ã¹ã® URL 表ç¾ããªã½ã¼ã¹ã®é¢ä¿ã®è¡¨ç¾æ¹æ³(ãªã½ã¼ã¹ãåãè¾¼ã¿ã§ã¬ã¹ãã³ã¹ããããã¤ãã¼ãªã³ã¯ã¨ãã¦ã¬ã¹ãã³ã¹ããã)ã¯ç°ãªãã¾ããä»åã¯ãã®ããã«è¨è¨ãã¾ãã
Rails new
ããã§ã¯å®éã«ããã°ã¢ããªã±ã¼ã·ã§ã³ãå®è£ ãã¦ããã¾ããGarage 㯠Rails ã® gemified plugin ãªã®ã§ã¢ããªã±ã¼ã·ã§ã³ã®ä½æã«ã¤ãã¦å¤æ´ããç¹ã¯ããã¾ããã
Garage ã¢ããªã±ã¼ã·ã§ã³ã®éçºã«ã¯å
¸åçã«ã¯ RSpec ã使ç¨ããã®ã§ãããã§ã¯ --skip-testunit ãã©ã°ãä»ã㦠rails new
ã³ãã³ããå®è¡ãã¾ãã
⯠rails new blog --skip-bundle --skip-test-unit -q && cd blog
éçºã«å¿ è¦ãª gem ã追å ãã¾ããç¾å¨ rubygems.org ã§ãã¹ãããã¦ãã garage gem ã¯ãã®è¨äºã§æ±ã£ã¦ãã Garage ã¨ã¯å¥ã® gem ã§ãã®ã§ãBundler ã® github shorthand ã使ã£ã¦ Garage ãæå®ãã¾ãã
# Gemfile +gem 'garage', github: 'cookpad/garage' + +group :development, :test do + gem 'factory_girl_rails', '~> 4.5.0' + gem 'pry-rails', '~> 0.3.2' + gem 'rspec-rails', '~> 3.1.0' +end +
bundle install 㨠rspec helper ã®è¨å®ãè¡ã£ã¦ããã¾ãã
⯠bundle install ⯠bundle exec rails g rspec:install
# spec/rails_helper.rb -# Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } +Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } # spec/spec_helper.rb -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin # These two settings work together to allow you to limit a spec run # to individual examples or groups you care about by tagging them with # `:focus` metadata. When nothing is tagged with `:focus`, all examples @@ -81,5 +78,4 @@ RSpec.configure do |config| # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed -=end end # spec/support/factory_girl.rb +RSpec.configure do |config| + config.include FactoryGirl::Syntax::Methods +end
Configuration and authentication/authorization
bundle install ã rspec helper ã®è¨å®ãçµããå¾ã¯ãGarage åæè¨å®ã¨èªå¯ã©ã¤ãã©ãªã® Doorkeeper ã®åæè¨å®ããã¾ãã
Doorkeeper 㯠Rails ã¢ããªã±ã¼ã·ã§ã³ã« OAuth 2 provider ã®æ©è½ãæãããããã® gem ã§ããGarage 㯠Doorkeeper ãç¨ãã¦èªå¯æ©è½ãæä¾ãã¾ãã
ã¯ãã¯ãããã§ã¯è¤æ°ã® Garage ã¢ããªã±ã¼ã·ã§ã³ãåå¨ãã¦ããã®ã§ãDoorkeeper ã使ç¨ããã«ãèªè¨¼èªå¯ãµã¼ãã¼ã¸èªè¨¼èªå¯ãå§è²ããã¢ã¸ã¥ã¼ã«ã Garage ã«è¿½å ãã¦ãã¾ãããã®ãã㪠Doorkeeper 以å¤ã®èªå¯å®è£ ãèªå¯ãè¡ããªãå®è£ ã¸ã®å¯¾å¿ã¯ Garage æ¬ä½ã¸è¿½å äºå®ã§ãã
ä»åå®è£ ããã¢ããªã±ã¼ã·ã§ã³ã§ã¯ Doorkeeper ãç¨ãã¦1ã¢ããªã±ã¼ã·ã§ã³å ã§èªè¨¼èªå¯ãå®çµããã¾ããconfig/initializers/garage.rb ãä½æããGarage 㨠Doorkeeper ã®è¨å®ã追å ãã¾ãã
# config/initializers/garage.rb +Garage.configure {} +Garage::TokenScope.configure {} + +Doorkeeper.configure do + orm :active_record + default_scopes :public + optional_scopes(*Garage::TokenScope.optional_scopes) + + resource_owner_from_credentials do |routes| + User.find_by(email: params[:username]) + end +end # config/routes.rb Rails.application.routes.draw do + use_doorkeeper end
Garage ã®è¨å®ã®éª¨çµã¿ã¨ä»åã®ã¢ããªã±ã¼ã·ã§ã³ç¨ã® Doorkeeper ã®è¨å®ã追å ãã¦ãã¾ããresource_owner_from_credentials
ã¡ã½ããã§è¨å®ãã¦ããã®ã¯ OAuth2 ã® Resource Owner Password Credentials Grant ã使ç¨ããæã®ã¦ã¼ã¶ã¼ã®èªè¨¼æ¹æ³ã§ããä»åã¯ç°¡åã«ããããããã¹ã¯ã¼ãç¡ãã§ã¡ã¼ã«ã¢ãã¬ã¹ã®ã¿ãç¨ãã¦èªè¨¼ãè¡ãã¾ãã
è¨å®ãå®ç¾©ããå¾ã¯ãDoorkeeper ãæä¾ãã migration ãçæãã¦å®è¡ãã¦ããã¾ãã
⯠bundle exec rails generate doorkeeper:migration ⯠bundle exec rake db:create db:migrate
Start with GET /v1/users
ã¾ãã¯ã³ã³ããã¼ã©ã¼ãã¢ãã«ã®ä½æãè¡ã£ã¦ãã¦ã¼ã¶ã¼æ å ±ã® GET ãã§ããã¨ããã¾ã§é²ãã¾ãã
ã³ã³ããã¼ã©ã¼ã®ä½æ
Rails æ¨æºã® ApplicationController
ãããã¯ãã§ã«å®è£
ããã¦ãã Rails ã¢ããªã±ã¼ã·ã§ã³ã« Garage ã使ç¨ããå ´åã¯ããã«æºããæ½è±¡ã³ã³ããã¼ã©ã¼ã¯ã©ã¹(ApiController
ãªã©)ã« Garage::ControllerHelper
ã include ãã¾ããControllerHelper
ã¯å
¨ã¦ã®ã³ã³ããã¼ã©ã¼ã«å
±éã®åºæ¬çãªãã£ã«ã¿ã¨ã¡ã½ãããæä¾ãã¾ããDoorkeeper ã使ç¨ããèªè¨¼ã¨èªå¯ãè¡ããã¾ãã
# app/controllers/application_controller.rb + + include Garage::ControllerHelper + + def current_resource_owner + @current_resource_owner ||= User.find(resource_owner_id) if resource_owner_id + end end
Garage ã®è¦ç´ã¨ãã¦ããã§ã¦ã¼ã¶ã¼ãå®ç¾©ãã¹ããã®ã¯ current_resource_owner
ã¨ããã¡ã½ããã§ãããã®ã¡ã½ããã¯ãªã¯ã¨ã¹ããããã¢ã¯ã»ã¹ãã¼ã¯ã³ã«ç´ä»ãã¦ãããªã½ã¼ã¹ãªã¼ãã¼æ
å ±ã使ç¨ãã¦ã¢ããªã±ã¼ã·ã§ã³ã®ã¦ã¼ã¶ã¼ãªãã¸ã§ã¯ãã¸å¤æãããã¨ãæå¾
ããã¦ãã¾ãã注æããç¹ã¨ãã¦ã¯ãOAuth2 ã® client credentials ãªã©ã® grant type ã§èªå¯ããã¢ã¯ã»ã¹ãã¼ã¯ã³ã«ã¤ãã¦ã¯ãªã½ã¼ã¹ãªã¼ãã¼æ
å ±ãç´ã¤ããªãã®ã§ nil ãå
¥ã£ã¦ãããã¨ãããã¾ããããã§å¤æããã¦ã¼ã¶ã¼ãªãã¸ã§ã¯ãã«å¯¾ãã¦å¾è¿°ããã¢ã¯ã»ã¹ã³ã³ããã¼ã«ãã¸ãã¯ãå®è¡ããã¾ãã
次ã«æ®æ®µã® Rails ã¢ããªã±ã¼ã·ã§ã³ã¨åããããªå½åè¦åã§ã¦ã¼ã¶ã¼ãªã½ã¼ã¹ã®æä¾ç¨ã« UsersController ãä½æãã¾ããroutes è¨å®ã¯æ®æ®µã® Rails ã¢ããªã±ã¼ã·ã§ã³ã¨åãã§ãã
# app/controllers/users_controller.rb +class UsersController < ApplicationController + include Garage::RestfulActions + + def require_resources + @resources = User.all + end +end # config/routes.rb Rails.application.routes.draw do use_doorkeeper + + scope :v1 do + resources :users, only: %i(index show update) + end end
ãªã½ã¼ã¹ãæä¾ããã³ã³ããã¼ã©ã¼ã§ã¯ Garage::RestfulActions
ã include ãã¦ãindex/create/show/update/delete ããããã«å¯¾å¿ãã require_resources/create_resource/require_resource/update_resource/destroy_resource ã¡ã½ãããå®ç¾©ãã¾ããããã§ã¯ index ã«å¯¾å¿ãã require_resources
ã¡ã½ãããå®ç¾©ãã¦ãã¾ããGarage::RestfulActions
ãã¦ã¼ã¶ã¼å®ç¾©ã® require_resources
ãªã©ã使ç¨ãã¦å®éã® action ãã©ããããå½¢ã§å®ç¾©ãã¦ããã¾ãã
ããã§ã¯ç´¹ä»ãã¾ããã§ããããä»ã«ã Garage ã¯ãã¼ã¸ãã¼ã·ã§ã³æ©è½ãªã©ãæä¾ãã¦ãã¾ããã³ã³ããã¼ã©ã¼ã§ respond_with_resources_options
ã¡ã½ããããªã¼ãã¼ã©ã¤ãã㦠paginate option ãæå¹ã«ãããã¨ã§ããªã½ã¼ã¹ã³ã¬ã¯ã·ã§ã³ã®ç·æ°ã次ã®ãã¼ã¸ã¸ã®ãªã³ã¯ãªã©ãã¬ã¹ãã³ã¹ãããã¨ãã§ãã¾ãããµã³ãã«ã¢ããªã±ã¼ã·ã§ã³ã§ã¯å®è£
ãã¦ããã®ã§ããã²ã覧ã«ãªã£ã¦ãã ããã
ã¢ãã«ã¨ãªã½ã¼ã¹ã®å®ç¾©
ActiveRecord ã¢ãã«ã¯ Rails æ¨æºã®ããæ¹ã§ä½æãã¾ããä»åã®ã¢ããªã±ã¼ã·ã§ã³ã§ã¯ã¦ã¼ã¶ã¼ã¯èªç±ã«è¨å®ã§ããååã¨èªè¨¼ç¨ã® email ã¢ãã¬ã¹ãæã¤ãã¨ã«ãã¾ãã
⯠bundle exec rails g model user name:string email:string ⯠bundle exec rake db:migrate
ã¢ãã«ã«ãªã½ã¼ã¹ã¨ãã¦ã®å®ç¾©ã追å ãã¾ããGarage ã¯ãã®å®ç¾©ãå©ç¨ãã¦ãªã½ã¼ã¹ã®ã·ãªã¢ã©ã¤ã¼ã¼ã·ã§ã³ãã¢ã¯ã»ã¹ã³ã³ããã¼ã«ãå®è¡ãã¾ãã
# app/models/user.rb class User < ActiveRecord::Base + include Garage::Representer + include Garage::Authorizable + + property :id + property :name + property :email + + def self.build_permissions(perms, other, target) + perms.permits! :read + end + + def build_permissions(perms, other) + perms.permits! :read + perms.permits! :write + end end # config/initializers/garage.rb Garage.configure {} -Garage::TokenScope.configure {} +Garage::TokenScope.configure do + register :public, desc: 'acessing publicly available data' do + access :read, User + access :write, User + end +end Doorkeeper.configure do
Garage::Representer
ããªã½ã¼ã¹ã®ã¨ã³ã³ã¼ãã£ã³ã°ã»ã·ãªã¢ã©ã¤ã¼ã¼ã·ã§ã³ãæä¾ããã¢ã¸ã¥ã¼ã«ã§ããproperty
ã§ãªã½ã¼ã¹ãæã¤å±æ§ã宣è¨ãã¾ããä»ã«ã link
ãç¨ãã¦ä»ã®ãªã½ã¼ã¹ã¸ã®ãªã³ã¯ã宣è¨ãããã¨ãã§ãã¾ãã詳ããã¯ãµã³ãã«ã¢ããªã±ã¼ã·ã§ã³ãåç
§ãã¦ãã ããã
Garage::Authorizable
ãã¢ã¯ã»ã¹ã³ã³ããã¼ã«æ©è½ãæä¾ããã¢ã¸ã¥ã¼ã«ã§ããã¢ã¯ã»ã¹ã³ã³ããã¼ã«ã«ã¤ãã¦ã¯å¾è¿°ããã®ã§ãããã§ã¯ãããªãã¯ãªãªã½ã¼ã¹ã¨ãã¦å®ç¾©ããã¦ããã¾ããåæ§ã« OAuth2 ã®ã¹ã³ã¼ãã«ããã¢ã¯ã»ã¹ã³ã³ããã¼ã«ã«ã¤ãã¦ãå¾è¿°ããã®ã§ããã§ã¯ãããªãã¯ãªå®ç¾©ã«ãã¦ããã¾ãã
ãã¼ã«ã«ãµã¼ãã¼ã§ãªã¯ã¨ã¹ãã試ã
ããã¾ã§ã§ã¦ã¼ã¶ã¼ãªã½ã¼ã¹ã® GET ãå®è£ ããã®ã§ãã¼ã«ã«ç°å¢ã§å®è¡ãã¦ã¿ã¾ãã
# ãã¹ãã¦ã¼ã¶ã¼ãä½æãã¾ã ⯠bundle exec rails runner 'User.create(name: "alice", email: "[email protected]")' ⯠bundle exec rails s
ãµã¼ãã¼ãèµ·åããã Doorkeeper ãæä¾ãã OAuth provider ã®æ©è½ãå©ç¨ãã¦ã¢ã¯ã»ã¹ãã¼ã¯ã³ãåå¾ãã¾ããhttp://localhost:3000/oauth/applications ãéãã¦ãã¹ãç¨ã« OAuth ã¯ã©ã¤ã¢ã³ããä½æã㦠client id 㨠client secret ãä½æãã¦ãã¢ã¯ã»ã¹ãã¼ã¯ã³ãçºè¡ãã¾ãã
⯠curl -u "$APPLICTION_ID:$APPLICATION_SECRET" -XPOST http://localhost:3000/oauth/token -d 'grant_type=password&[email protected]' {"access_token":"XXXX","token_type":"bearer","expires_in":7200,"scope":"public"}
åå¾ããã¢ã¯ã»ã¹ãã¼ã¯ã³ã使ã£ã¦å ã»ã©å®è£ ããã¦ã¼ã¶ã¼ãªã½ã¼ã¹ãåå¾ãã¾ãã
⯠curl -s -XGET -H "Authorization: Bearer XXXX" http://localhost:3000/v1/users | jq '.' [ { "id": 1, "name": "alice", "email": "[email protected]" } ]
You're done!!
èªåãã¹ã
Garage ã¢ããªã±ã¼ã·ã§ã³ãéçºããä¸ã§èªåãã¹ããã»ããã¢ããããã¾ã§ã«ããã¤ã注æããç¹ãããã®ã§ãããã§ã¯ request spec ãå®è¡ããã¾ã§ã®ã»ããã¢ãããç´¹ä»ãã¾ãã
Doorkeeper ã«ããèªè¨¼èªå¯ãã¹ã¿ãããããã®ãã«ãã¼ã追å ãã¾ãã
# spec/support/request_helper.rb +require 'active_support/concern' + +module RequestHelper + extend ActiveSupport::Concern + + included do + + let(:params) { {} } + + let(:env) do + { + accept: 'application/json', + authorization: authorization_header_value + } + end + + let(:authorization_header_value) { "Bearer #{access_token.token}" } + + let(:access_token) do + FactoryGirl.create( + :access_token, + resource_owner_id: resource_owner.id, + scopes: scopes, + application: application + ) + end + + let(:resource_owner) { FactoryGirl.create(:user) } + let(:scopes) { 'public' } + let(:application) { FactoryGirl.create(:application) } + end +end
RSpec example group ã®ä¸ã§å¿
è¦ã«å¿ã㦠resource_owner
ã scopes
ãä¸æ¸ãå®ç¾©ãããã¨ã§ããªã½ã¼ã¹ãªã¼ãã¼ã®éãã OAuth2 ã®ã¹ã³ã¼ãã®éããä½ãã ãã¾ãã
ã¤ãã§ã«ç´°ããã¨ããã§ãããfacotry ã®å®ç¾©ãæ¸ãæãã¦ããã¾ãã
# spec/factories/users.rb FactoryGirl.define do factory :user do - name "MyString" -email "MyString" + sequence(:name) {|n| "user#{n}" } + email { "#{name}@example.com" } end - end
æåã® request spec ã¯æå°éã®ãã¹ãã®ã¿å®è¡ããããã«ãã¾ãã
# spec/requests/users_spec.rb +require 'rails_helper' + +RSpec.describe 'users', type: :request do + include RequestHelper + + describe 'GET /v1/users' do + let!(:users) { create_list(:user, 3) } + + it 'returns user resources' do + get '/v1/users', params, env + expect(response).to have_http_status(200) + end + end +end
ãã¹ãç¨ãã¼ã¿ãã¼ã¹ãä½æãã¦ãã¹ãå®è¡ãã¦ã¿ã¾ãã
⯠RAILS_ENV=test bundle exec rake db:create migrate ⯠bundle exec rspec -fp spec/requests/users_spec.rb Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} . Finished in 0.06393 seconds (files took 1.67 seconds to load) 1 example, 0 failures
ç¡äºèªåãã¹ãã®ã»ããã¢ãããã§ãã¾ããã
ãããã¹ãã³ã¼ãã DRY ã«ããã«ã¯ rspec-request_describer gem ãå°å ¥ãããã¨ãããããã§ãã
ãªã½ã¼ã¹ã®ä¿è·
å®éã® Web API ã§ã¯ãªã½ã¼ã¹ã«å¯¾ããã¢ã¯ã»ã¹ã³ã³ããã¼ã«ã権éè¨å®ãéè¦ã«ãªãã¾ããå ·ä½çã«ã¯ãã¦ã¼ã¶ã¼ãèªå¯ããã¯ã©ã¤ã¢ã³ãã«ã®ã¿ãã©ã¤ãã¼ããªã¡ãã»ã¼ã¸ã®èªã¿æ¸ãã許å¯ãããããããã¯ããã°è¨äºã®ç·¨éã¯æ稿è èªèº«ã«ã®ã¿è¨±å¯ãããã¨ãã£ãä¾ãããã¾ãã
Garage ã§ã¯ OAuth2 å©ç¨ããã¢ã¯ã»ã¹æ¨©ã®è¨å®ããªã½ã¼ã¹æ¯ã«å®ç¾©ãããã¨ã¨ããªã¯ã¨ã¹ãã³ã³ããã¹ãã使ç¨ãã¦ãªã½ã¼ã¹æä½ã«å¯¾ãã権éããªã½ã¼ã¹æ¯ã«å®ç¾©ãããã¨ã§ããªã½ã¼ã¹ã®ä¿è·ãå®ç¾ã§ãã¾ãã
ã¢ã¯ã»ã¹ã³ã³ããã¼ã«ã«ã¤ãã¦ã¯æ¦å¿µèªä½ãããã«ããã¨æãã®ã§ãå®éã«åãã¢ããªã±ã¼ã·ã§ã³ã§è©¦ãã¦ã¿ã¾ãããµã³ãã«ã¢ããªã±ã¼ã·ã§ã³ã¢ããªã±ã¼ã·ã§ã³ã®ãªãã¸ã§ã³ 1b3e35463b87631d1e6acdd08e11ae09cab1b7cc
ããã§ãã¯ã¢ã¦ããã¾ãã
git clone [email protected]:taiki45/garage-example.git && cd garage-example git checkout 1b3e35463b87631d1e6acdd08e11ae09cab1b7cc
ããã§ã¯ã¦ã¼ã¶ã¼ãªã½ã¼ã¹ã«å¯¾ãã¦ãªã½ã¼ã¹æä½ã®æ¨©éè¨å®ããã¦ã¿ã¾ããä»äººã®ã¦ã¼ã¶ã¼æ å ±ã¯é²è¦§ã§ãããä»äººã®ã¦ã¼ã¶ã¼æ å ±ã¯å¤æ´ã§ããªããã¨ããæåã«å¤æ´ãã¾ãããã¹ãã¨ãã¦ã¯æ¬¡ã®ããã«æ¸ãã¾ãã
# spec/requests/users_spec.rb describe 'PUT /v1/users/:user_id' do before { params[:name] = 'bob' } context 'with owned resource' do let!(:user) { resource_owner } it 'updates user resource' do put "/v1/users/#{user.id}", params, env expect(response).to have_http_status(204) end end context 'without owned resource' do let!(:other) { create(:user, name: 'raymonde') } it 'returns 403' do put "/v1/users/#{other.id}", params, env expect(response).to have_http_status(403) end end end
ãã¹ãã失æãããã¨ã確ããã¾ãã
⯠bundle exec rspec spec/requests/users_spec.rb:24 users PUT /v1/users/:user_id with owned resource updates user resource without owned resource returns 403 (FAILED - 1)
ã¦ã¼ã¶ã¼ãªã½ã¼ã¹ã®ãã¼ããã·ã§ã³çµã¿ç«ã¦ãã¸ãã¯ãå¤æ´ãã¾ããother
ã¯ãªã¯ã¨ã¹ãã«ããããªã½ã¼ã¹ãªã¼ãã¼ãæç¸ããã¾ãããªã½ã¼ã¹ãªã¼ãã¼ã¯å
ã»ã© ApplicationController ã§å®è£
ãã current_resource_owner
ã¡ã½ããã§å¤æãããã¢ããªã±ã¼ã·ã§ã³ã®ã¦ã¼ã¶ã¼ãªãã¸ã§ã¯ããæç¸ããã¦ããã®ã§ãä»åã®ã¢ããªã±ã¼ã·ã§ã³ã 㨠User ã¯ã©ã¹ã®ã¤ã³ã¹ã¿ã³ã¹ã§ãã
# app/models/user.rb def build_permissions(perms, other) perms.permits! :read - perms.permits! :write + perms.permits! :write if self == other end
ãã¹ããå®è¡ãã¦ã¿ã¾ãã
⯠bundle exec rspec spec/requests/users_spec.rb:24 users PUT /v1/users/:user_id without owned resource returns 403 with owned resource updates user resource
ä»äººã®ã¦ã¼ã¶ã¼æ å ±ã¯æ´æ°ã§ããªãããã«ã§ãã¾ããã
Garage ã¯ä»ã«ãããã°ã®ä¸æ¸ãæ稿ã¯æ稿è ããé²è¦§ã§ããªããã¦ã¼ã¶ã¼ã®ååã®å¤æ´ã¯ç¹å®ã®ã¹ã³ã¼ãããªãã¨å¤æ´ã§ããªãããªã©æ§ã ãªæ¨©éè¨å®ãã§ãã¾ããããã§ã¯ç´¹ä»ãã¾ããã§ããããã¢ã¯ã»ã¹æ¨©ã®è¨å®ãããã¤ãã®æ¡å¼µæ©è½ã«ã¤ãã¦ã¯ãµã³ãã«ã¢ããªã±ã¼ã·ã§ã³ã¨ããã¥ã¡ã³ããåç §ãã¦ãã ããã
Response matcher
JSON API ã®ã¬ã¹ãã³ã¹ã®ãã¹ã㯠RSpec2 ã§ã¯ rspec-json_matcher ãç¨ãã¦ãRSpec3 ã§ã¯ composing-matchers ã使ç¨ãã¦è¨è¿°ãã¾ãããã¹ãã«ãã£ã¦ã¯æ§é ãæ¤æ»ããã ãã§ãªããå®éã®ã¬ã¹ãã³ã¹ãããå¤ãæ¤æ»ãã¾ãã
RSpec2
let(:post_structure) do { 'id' => Integer, 'title' => String, 'body' => String, 'published_at' => String } end describe 'GET /v1/posts/:post_id' do let!(:post) { create(:post, user: resource_owner) } it 'returns post resource' do get "/v1/posts/#{post.id}", params, env response.status.should == 200 response.body.should be_json_as(post_structure) end end
RSpec3
let(:post_structure) do { 'id' => a_kind_of(Integer), 'title' => a_kind_of(String), 'body' => a_kind_of(String).or(a_nil_value), 'published_at' => a_kind_of(String).or(a_nil_value) } end describe 'GET /v1/posts/:post_id' do let!(:post) { create(:post, user: resource_owner) } it 'returns post resource' do get "/v1/posts/#{post.id}", params, env expect(response).to have_http_status(200) expect(JSON(response.body)).to match(post_structure) end end
DebugExceptions
Rails ã¯ããã©ã«ãã®è¨å®ã 㨠development ç°å¢ã§ã¯ãµã¼ãã¼ã¨ã©ã¼ãèµ·ããå ´åã ActionDispatch::DebugExceptions
ãã¨ã©ã¼æ
å ±ã HTML ã§ã¬ã¹ãã³ã¹ãã¾ããJSON API éçºã®æèã§ã¯ãããã°ç¨ã®ã¨ã©ã¼ã¬ã¹ãã³ã¹ã JSON ã®ã»ããé½åãè¯ãã§ãããã®å ´å debug_exceptions_json gem ã使ãã¾ããã¨ã©ã¼æ
å ±ã JSON ã§ã¬ã¹ãã³ã¹ãããã®ã§ãéçºããããããªãã¾ããã¾ããRSpec ã¨ã®é£æºæ©è½ããããrequest spec ã®å®è¡ä¸ã«ãµã¼ãã¼ã¨ã©ã¼ãèµ·ãã㨠RSpec ã®ãã©ã¼ããã¿ãå©ç¨ãã¦ã¨ã©ã¼æ
å ±ããã³ããã¦ããã¾ãã
Failures: 1) server error dump when client accepts application/json with exception raised responses error json Failure/Error: expect(response).to have_http_status(200) expected the response to have status code 200 but it was 500 # ./spec/features/server_error_dump_spec.rb:21:in `block (4 levels) in <top (required)>' ServerErrorDump: exception class: HelloController::TestError message: test error short_backtrace: <backtrace is here>
APIããã¥ã¡ã³ã
Web API éçºã®æèã§ã¯ãAPI ã®ããã¥ã¡ã³ããæä¾ãããã¥ã¡ã³ããææ°ã®ç¶æ ã«ã¢ãããã¼ããã¦ãããã¨ã§éçºä¸ã®ã³ãã¥ãã±ã¼ã·ã§ã³ãå¹çåã§ãã¾ãã
API ããã¥ã¡ã³ãã®çæã«ã¯ autodoc gem ã使ã£ã¦ãã¾ãããªã¯ã¨ã¹ãä¾ãã¬ã¹ãã³ã¹ä¾ã ãã§ãªããweak_parameters gem ã¨çµã¿åããããã¨ã§ãªã¯ã¨ã¹ããã©ã¡ã¼ã¿ã«ã¤ãã¦ãããã¥ã¡ã³ãåã§ãã¾ããçæãããããã¥ã¡ã³ã㯠Garage ã®ããã¥ã¡ã³ãæä¾æ©è½ã使ç¨ãã¦ãã©ã¦ã¶ã§é²è¦§ã§ããããã«ãããã¨ãã§ãã¾ãããããç°¡åã«ã¯ markdown ãã©ã¼ãããã§çæãããã®ã§ Github ä¸ã§ã¬ã³ãã¼ãããããã¥ã¡ã³ããåç §ãã¦ããããã¨ãã§ãã¾ãã
Garage ã使ç¨ãã RESTful Web API ã®éçºã«ã¤ãã¦ãç´¹ä»ãã¾ãããã³ã³ããã¼ã©ã¼ãä½ãããªã½ã¼ã¹ãå®ç¾©ãããã¢ã¯ã»ã¹ã³ã³ããã¼ã«ãå®ç¾©ããããã®ã¹ããããç¹°ãè¿ããã¨ã§ã¢ããªã±ã¼ã·ã§ã³ãéçºãããã¨ãã§ãã¾ãã
ãã®è¨äºã Garage ã使ç¨ããã¢ããªã±ã¼ã·ã§ã³å®è£ ã®åèã«ãªãã°å¹¸ãã§ãã