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

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

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

参考