Cookpadさんが、作られているcookpad/garageについて調べて、使ってみましたー
RESTful Hypermedia APIとは?
まずは、Hypermedia APIについて調べて見ました。
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
を使ってます。
導入方法
gem 'garage', github: 'cookpad/garage'
gem 'responders', '~> 2.0' # If you use Rails4.2+
$ 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 で行う為、紛らわしいので一応コメントを残します。
# Doorkeeper configuration is merged into 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の設定
設定はあくまでも一例です。
適宜直してお使いください。
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 を参考にして使ってください。
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の設定
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用に書き換えました。
gem "link_header"
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
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
こんな感じで使います
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