RESTful Hypermedia API サーバサイド編 – garage

モバイルアプリサービス部の五十嵐です。

前回の記事ではハイパーメディアAPIの概要について説明しました。これから何回かにわたって、Ruby on RailsでハイパーメディアAPIを実装するためのgemを紹介したいと思います。今回はcookpadさんが提供されているgarageというgemを試してみます。

Garageとは

Garageは、Ruby on RailsにRESTful hypermedia APIを追加するgemです。

Rails framework to add RESTful hypermedia API to your application.

GarageはRailsネイティブのRESTfulなルーティングを使い、シンプルなHypermediaフレンドリーなRESTful APIを提供します。中略。またDoorkeeperなどを使用したOauth2認証や、リソースベースのアクセスコントロールを提供します。

Garage provides a simple, Hypermedia friendly RESTful API to your Rails application using its native RESTful routes. Garage provides a descriptive way to serve your ActiveRecord models, as well as plain old Ruby objects as JSON-based resources.

Garage supports OAuth 2 authorizations via Doorkeeper (more extensions to come), and provides resource-based access controls.

(公式リポジトリのREADME.mdより)

サンプルプログラム

クックパッド開発者ブログで紹介されている手順を参考にしつつ、Hypermediaフレンドリーな機能も試してみたいと思います。実装する内容はブログの通りですが、以下に抜粋を記載します。

・アプリケーションが提供するリソースはログインユーザーである 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の実装なので、認証やテストなど本題と外れるところは省略していきます。今回のサンプルコードはGitHubのリポジトリにありますので参考にしてください。サンプルコードには簡単なテストも書いています。

動作環境

  • Ruby (2.2.2)
  • rails (4.2.1)
  • garage (1.5.2)

Rails new

まずアプリケーションを作成します。

rails new blog --skip-bundle --skip-test-unit -q && cd blog

次にGemfileに必要なgemを設定していきます。何点か注意がありますが、まずgarageはGitHubのcookpad/garageを参照してください。rubygemsに登録されているgarageは全く別物です。また、今回は省略しますが認証にdoorkeeperを使用される場合、最新のgarageではdoorkeepergarageに含まれませんので、別途garage-doorkeeperを追加する必要があります。respondersgarageが使うので設定しておきます。

# Gemfile
gem 'garage', github: 'cookpad/garage'
gem 'responders', '~> 2.0'

設定したらbundle installを行います。

bundle install

Configuration and authentication/authorization

次にgarageの設定を行います。Garage.configureにはgarage本体の設定をします。Garage::TokenScope.configureにはアクセスコントロールの設定を行います。Garage.configuration.strategyには認証の方法を設定します。今回は認証は行わないので、Garage::Strategy::NoAuthenticationを設定します。

# config/initializers/garage.rb
Garage.configure {}

Garage::TokenScope.configure {}

Garage.configuration.strategy = Garage::Strategy::NoAuthentication

コントローラの作成

applicationコントローラにはGarage::ControllerHelperを追加します。これはコントローラ共通で使用するフィルタとメソッドが提供されます。今回は使用しませんがお決まりの書き方のようなので書いておきます。

# 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

usersコントローラとpostsコントローラを作成します。

bundle exec rails g controller users
bundle exec rails g controller posts

コントローラにGarage::RestfulActionsをincludeすることで、index/create/show/update/deleteメソッドそれぞれをラップしたrequire_resources/create_resource/require_resource/update_resource/destroy_resourceメソッドが使えるようになります。

# app/controllers/users_controller.rb
include Garage::RestfulActions

# index
def require_resources
@resources = User.all
end

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

# update
def update_resource
@resource.update_attributes!(user_params)
end

private

def user_params
params.permit(:name)
end
# app/controllers/posts_controller.rb
include Garage::RestfulActions

# index
def require_resources
if params[:user_id]
@resources = User.find(params[:user_id]).posts
else
@resources = Post.all
end
end

# create
def create_resource
@resources.create(post_params.merge(user_id: resource_owner_id))
end

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

# update
def update_resource
@resource.update_attributes!(post_params)
end

# destroy
def destroy_resource
@resource.destroy!
end

private

def post_params
params.permit(:title, :body, :published_at)
end

ルーティングを設定します。postリソースはuserリソースにネストされるようにし、必要なアクションだけを設定しておきます。

# config/routes.rb
scope :v1 do
resources :users, only: %i(index show update) do
resources :posts, shallow: true, except: %i(new edit)
end
end

モデルとリソースの定義

userモデルとpostモデルを作成し、マイグレーションします。

bundle exec rails g model user name:string email:string
bundle exec rails g model post title:string body:string published_at:datetime user:references
bundle exec rake db:migrate

モデルでレスポンスの内容を定義します。propertyは属性、linkはリンク、collectionはアソシエーションしたモデルの情報を返します。selectableオプションをtrueにすると、デフォルトでは値が返らず、リクエストで何らかのパラメータを与えることで返るようになるのだと思いますがパラメータの与え方が分かりませんでした。

# app/model/user.rb
include Garage::Representer

has_many :posts

property :id
property :name
property :email

link(:posts) { user_posts_path(self) }

collection :posts
# app/model/post.rb
include Garage::Representer

belongs_to :user

property :id
property :title
property :body
property :published_at
property :user, selectable: true

ローカルサーバーでリクエストを試す

テストデータを準備します。

bundle exec rails c
user = User.create(name: "name1", email: "mail1@example.com")
user.posts.create(title: 'title1', body: 'body1', published_at: DateTime.now)
user.posts.create(title: 'title2', body: 'body2', published_at: DateTime.now)
user.posts.create(title: 'title3', body: 'body3', published_at: DateTime.now)

サーバを起動します。

bundle exec rails s

ターミナルからcurlで/v1/users/:idにGETリクエストするとjsonデータが返されます。モデルに設定したpropertylinkcollection、それぞれが表示されているのが分かりますでしょうか。

curl -s http://localhost:3000/v1/users/1 | jq .
{
"id": 1,
"name": "name1",
"email": "mail1@example.com",
"_links": {
"posts": {
"href": "/v1/users/1/posts"
}
},
"posts": [
{
"id": 1,
"title": "title1",
"body": "body1",
"published_at": "2015-09-23T15:23:53.828Z"
},
{
"id": 2,
"title": "title2",
"body": "body2",
"published_at": "2015-09-23T15:24:18.783Z"
},
{
"id": 3,
"title": "title3",
"body": "body3",
"published_at": "2015-09-23T15:24:25.218Z"
}
]
}

所感

今回はgarageのAPI機能とハイパーメディア要素の作り方の一部を紹介しました。ご覧頂いた通り、簡単にハイパーメディアなAPIを作成することができました。クックパッドさんのサンプルコードを見たところ、他にもページネーションの要素を出力する方法などがありそうでしたが、それ以上の情報がなく試すところまでは至りませんでした。サンプルコードの時点からも結構バージョンアップしていて書き方とかも変わっていて、今まさに開発中という感じでしたのでこれからに期待しましょう!また、今回は省略した認証やアクセスコントロールは別の機会に紹介したいと思います。

参考