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

Cookpadさんが、作られている[cookpad/garage](https://github.com/cookpad/garage)について調べて、使ってみましたー

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

## Hypermedia APIとは？
[What Is A Hypermedia API?](http://apievangelist.com/2014/01/07/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とのコミュニケーションをとるのに共通の手法がある
 - デベロッパーがどのようなアクションをとれるか導いてくれる

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

[クックパッドとマイクロサービス](http://techlife.cookpad.com/entry/2014/09/08/093000)
こちらを読んでやっと意味が掴めてきました。

```
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](https://github.com/cookpad/garage#inspired-by)に書かれてるように、

 - roar
 - doorkeeper

を使ってます。

## 導入方法

```ruby:Gemfile
gem 'garage', github: 'cookpad/garage'
gem 'responders', '~> 2.0' # If you use Rails4.2+
```

```bash: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 で行う為、紛らわしいので一応コメントを残します。

```ruby:config/initializers/doorkeeper.rb
# Doorkeeper configuration is merged into garage.rb
```

```ruby: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の設定
設定はあくまでも一例です。
適宜直してお使いください。

```ruby: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 を参考にして使ってください。

```ruby: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の設定
```ruby: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用に書き換えました。

```ruby:Gemfile
gem "link_header"
```

```ruby: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
```

```ruby: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
```

こんな感じで使います

```ruby: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