LoginSignup
77
78

More than 5 years have passed since last update.

RESTful Hypermedia APIをRailsで実現するcookpad/garageが凄い

Last updated at Posted at 2014-10-20

Cookpadさんが、作られているcookpad/garageについて調べて、使ってみましたー

RESTful Hypermedia APIとは?

まずは、Hypermedia APIについて調べて見ました。

Hypermedia APIとは?

What Is A Hypermedia API?

こちらから重要な部分を引っ張ってきます。

  • Shared, common way that developers can communicate with the API
  • Guiding developers with what actions they can take along the way

著者訳

  • デベロッパーがAPIとのコミュニケーションをとるのに共通の手法がある
  • デベロッパーがどのようなアクションをとれるか導いてくれる

何を言っているのか理解ができませんね。。

クックパッドとマイクロサービス
こちらを読んでやっと意味が掴めてきました。

RESTful JSON APIと一言で表現しても、厳密な規格があるわけではありません。
ですから、アプリケーションごとにそれぞれAPIを実装すると、
まったく異なった設計思想にもとづくバラバラなAPIができあがってしまいます。

ふむふむ、

デベロッパーがAPIを実装すると規格が存在せず、設計思想が違うAPIができてしまう。
それを解決する為に共通の手法を指南するのがHypermedia API。
で、Hypermedia APIのRESTful版が, RESTful Hypermedia API。

って事みたいですねー。
なるほどー

garage

garageとは?

Twitterなどが導入しているパスワードを入力させずとも他のサービスで、アクセストークンを用い、そのアプリとの連携をできる仕組み OAuth を使用したAPIを作る事のできるライブラリが、garageです。
RESTful Hypermedia APIを実現し、Doorkeeper Gemをベースに更に使いやすくなっています。

依存

Inspired Byに書かれてるように、

  • roar
  • doorkeeper

を使ってます。

導入方法

Gemfile
gem 'garage', github: 'cookpad/garage'
gem 'responders', '~> 2.0' # If you use Rails4.2+
shell
 $ bundle
 $ bundle exec rails generate doorkeeper:install
 $ bundle exec rails generate doorkeeper:migration
 $ bundle exec rails generate model users
 $ bundle exec rake db:migrate

Doorkeeper gemの初期化も
config/initializers/garage.rb で行う為、紛らわしいので一応コメントを残します。

config/initializers/doorkeeper.rb
# Doorkeeper configuration is merged into garage.rb
config/initializers/garage.rb
Garage.configure do
  rescue_error = false
end

Garage::TokenScope.configure do
  # register :hogehoge で、scopeの宣言
  # accessで何をできるのかを指定

  # register :public do
  #   access :read, MainTopic
  #   access :read, Topic
  #   access :read, Response
  # end
  # 
  # register :write_topic do
  #   access :write, Topic
  #   access :write, Response
  # end
  # 
  # register :write_response do
  #   access :write, Response
  # end
end

Doorkeeper.configure do
  orm :active_record

  # アプリケーションのオーナーの認証
  resource_owner_authenticator do
    User.find_by_id(session[:id]) || redirect_to(new_session_url)
    # for devise - current_user || redirect_to(new_user_session_path)
  end

  # デフォルトで持つスコープ
  default_scopes :public
  # 任意で持つスコープ
  optional_scopes *Garage::TokenScope.optional_scopes
end

controllerの設定

設定はあくまでも一例です。
適宜直してお使いください。

app/controllers/api/api_controller.rb
class Api::ApiController < ApplicationController
  include Garage::ControllerHelper
  skip_before_action :verify_authenticity_token

  def current_resource_owner
    @current_resource_owner ||= User.find(resource_owner_id) if resource_owner_id
    # for devise - @current_resource_owner ||= User.find(resource_owner_id) if user_signed_in?
  end

  def resource_owner_exists?(resource_owner_id)
    User.exists?(resource_owner_id)
  end
end

gem側の問題で、@resource, @resources という名前は固定のようです。
https://github.com/cookpad/garage/blob/master/doc/restful_actions.md を参考にして使ってください。

app/controllers/api/v1/rest/main_topics_controller.rb
module Api::Rest::V1
  class MainTopicsController < Api::ApiController
    include Garage::RestfulActions

    # /index
    def require_resources
      @resources = MainTopic.all
    end

    # /show
    def require_resource
      @resource = MainTopic.find(params[:id])
    end
  end
end

Modelの設定

app/models/main_topic.rb
class MainTopic < ActiveRecord::Base
  # 必須
  include Garage::Representer
  # build_permissions を使う際に必要
  include Garage::Authorizable

  property :id
  property :image_url
  property :en_name

  # :index で必要
  def self.build_permissions(perms, other, target)
    perms.permits! :read
  end

  # :create, :update, :show, :destroy で必要
  def build_permissions(perms, other)
    perms.permits! :read
  end
end

使い方

OAuthクライアント、トークンの発行の仕方など後日追記

RSpec 3でのテスト

こいつがかなり苦労しました。
garageは、RSpec 2でテストが書かれていますが、RSpec 3を使っていたので、本当に辛かったです。
(僕のRSpecの問題解決能力が皆無なだけかもしれない)

https://github.com/cookpad/garage/tree/master/spec/support
これを参考にして、RSpec 3用に書き換えました。

Gemfile
gem "link_header"
spec/support/authenticated_context.rb
require "link_header"

module RestApiSpecHelper
  extend ActiveSupport::Concern

  def link_for(rel)
    value = response.header["Link"]
    parsed = LinkHeader.parse(value)
    link = parsed.find_link(["rel", rel])
    link && Rack::Utils.parse_query(URI.parse(link.href).query).symbolize_keys
  end

  def execute
    body = params.presence
    body = params.to_json if body && env["CONTENT_TYPE"].try(:include?, "application/json")
    send(method, path, body, env)
  end

  included do
    let(:example) { |ex| ex }

    let(:link) do
      LinkHeader.parse(response.header["Link"])
    end

    let(:params) do
      {}
    end

    let(:header) do
      { "Accept" => "application/json" }
    end

    let(:env) do
      header.inject({}) do |table, (key, value)|
        table.merge("HTTP_#{key.upcase.gsub(?-, ?_)}" => value.to_s)
      end
    end

    let(:method) do
      example.full_description[/ (GET|POST|PUT|DELETE) /, 1].downcase
    end

    let(:path) do
      example.full_description[/ (?:GET|POST|PUT|DELETE) (.+?)(?: |$)/, 1].gsub(/:([^\s\/]+)/) { send($1) }
    end
  end
end
spec/support/rest_api_spec_helper.rb
require "link_header"

module RestApiSpecHelper
  extend ActiveSupport::Concern

  def link_for(rel)
    value = response.header["Link"]
    parsed = LinkHeader.parse(value)
    link = parsed.find_link(["rel", rel])
    link && Rack::Utils.parse_query(URI.parse(link.href).query).symbolize_keys
  end

  def execute
    body = params.presence
    body = params.to_json if body && env["CONTENT_TYPE"].try(:include?, "application/json")
    send(method, path, body, env)
  end

  included do
    let(:example) { |ex| ex }

    let(:link) do
      LinkHeader.parse(response.header["Link"])
    end

    let(:params) do
      {}
    end

    let(:header) do
      { "Accept" => "application/json" }
    end

    let(:env) do
      header.inject({}) do |table, (key, value)|
        table.merge("HTTP_#{key.upcase.gsub(?-, ?_)}" => value.to_s)
      end
    end

    let(:method) do
      example.full_description[/ (GET|POST|PUT|DELETE) /, 1].downcase
    end

    let(:path) do
      example.full_description[/ (?:GET|POST|PUT|DELETE) (.+?)(?: |$)/, 1].gsub(/:([^\s\/]+)/) { send($1) }
    end
  end
end

こんな感じで使います

spec/requests/api/rest/v1/main_topics_spec.rb
require 'rails_helper'

RSpec.describe "MainTopic Rest API v1" do
  include AuthenticatedContext
  include RestApiSpecHelper

  describe 'GET /api/rest/v1/main_topics' do
    context 'hogehoge' do
      subject { execute }
      it '200' do
        expect(subject).to eq(200)
      end
    end
  end

  describe 'GET /api/rest/v1/main_topics/:id' do
    context '要素が存在しない' do
      let(:id){ 9999 }
      subject { execute }

      it '404' do
        expect(subject).to eq(404)
      end
    end
  end
end

以上です。
気軽に編集リクエストを送ってくださいー
コメント、わかりづらい点などもお待ちしております―
OAuthクライアントの作り方など随時更新していきます。

Thanks: @KaitoMiyazaki, @sakuraiben

77
78
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
77
78