samuelrodgers752 | Flickr - Photo Sharing!
RoarはRepresenterを使ってRESTなAPIをパース、レンダーすることができるgemです。
つまり、Roarを使うことで、RailsでJSONを返すAPIサーバーを作成したり、逆に、APIサーバーにアクセスするAPIクライアントをRubyで作れます。
下記に記載しましたが、有名なgemに比べて、メリットとしては、Rubyでサーバーとクライアントを作る場合、同じような箇所を幾分か共有できる点です。
デメリットとしては、個人的にパースやレンダー時にエラーが発生してもデバッグしづらく対処しづらいことです。
サーバー側でAPIを作る場合、「Ruby Toolbox - API Builders」によると、jbuilderやGrape、Rablなどが人気のようです。
APIにアクセスするクライアントを作る場合、「Ruby Toolbox - HTTP Clients」によるとRest-ClientやFaradayなどが人気です。
目次
動作確認
- Rails 4.2.3
- Ruby 2.2.0
- Roar 1.0.1
1. Roarの簡単な使い方
1.1. Railsにインストール
Gemfile
に追加します。
gem 'roar-rails'
bundle install
を実施すれば完了です。
1.2. Representerの定義
rails g representer
コマンドでRepresenterを作ることができます。
rails g representer Tweet id content create app/representers/tweet_representer.rb
app/representers
配下にRpresenterが作成されます。
property
でRepresenterでレンダーやパースする値を定義します。
# app/representers/user_representer.rb module TweetRepresenter include Roar::JSON property :id property :content end
Representerの定義では、他にも、パースやレンダー時に値を変換したり、パースやレンダーをスキップしたりといろいろとカスタマイズができるので、
困ったら以下のREADMEを読むと良いと思います。
1.3. レンダー(JSON, Hash, XML)
定義したRepresenterをextend
すし、to_json
、to_hash
メソッドを呼ぶことで、JSONやHashを出力することができます。TweetRepresenter
でid
とcontent
を定義しているのでその2つしか出力されません。
class Tweet < ActiveRecord::Base; end tweet = Tweet.create(content: 'Hoge') tweet.extend(::TweetRepresenter) tweet.to_json # =>"{\"id\":1,\"content\":\"Hoge\"}" tweet.to_hash # => {"id"=>1, "content"=>"Hoge"}
また、RailsでJSONを返したい場合は、次のようにします。
render json:
は引数に渡したオブジェクトのto_json
メソッドを呼び出した結果を返します。
そのため、Representerで定義したid
とcontent
のみが返されます。
# app/controllers/api/tweets_controller.rb class Api::TweetsController < ApplicationController skip_before_action :verify_authenticity_token def show tweet = Tweet.find(params[:id]).extend(::TweetRepresenter) render json: tweet # =>"{\"id\":1,\"content\":\"Hoge\"}" end end
XML形式で出力したい場合は、RepresenterにRoar::XML
をincludeし、to_xml
メソッドを呼び出します。
class Tweet < ActiveRecord::Base; end module TweetRepresenter include Roar::XML property :id property :content end tweet = Tweet.last tweet.extend(::TweetRepresenter) tweet.to_xml #=> "<tweet>\n <id>1</id>\n <content>Hoge</content>\n</tweet>"
1.4. パース(JSON, Hash, XML)
定義したRepresenterをextend
すし、to_json
、to_hash
メソッドを呼ぶことで、JSONやHashを出力することができます。TweetRepresenter
でid
とcontent
を定義しているのでその2つしか出力されません。
class Tweet < ActiveRecord::Base; end tweet = Tweet.new.extend(::TweetRepresenter) tweet.from_json("{\"id\":1,\"content\":\"Hoge\"}") # => #<Tweet id: 1, content: "Hoge", created_at: nil, updated_at: nil> tweet = Tweet.new.extend(::TweetRepresenter) tweet.from_hash({ 'id' => 1, 'content' => 'Hoge' }) # => #<Tweet id: 1, content: "Hoge", created_at: nil, updated_at: nil> # from_hashはHashのキーがシンボルの場合うまく認識してくれません。 tweet = Tweet.new.extend(::TweetRepresenter) tweet.from_hash({ id: 1, content: 'Hoge' }) # => #<Tweet id: nil, content: nil, created_at: nil, updated_at: nil> # with_indifferent_accessを使いましょう tweet.from_hash({ id: 1, content: 'Hoge' }.with_indifferent_access) # => #<Tweet id: 1, content: "Hoge", created_at: nil, updated_at: nil>
また、RailsでJSONやHashを受け取って、オブジェクトを作成したい場合は、from_json
やfrom_hash
を使います。
Representerで定義したid
とcontent
のみが取得してオブジェクトを作成します。
# app/controllers/api/tweets_controller.rb class Api::TweetsController < ApplicationController skip_before_action :verify_authenticity_token def create tweet = Tweet.new.extend(::TweetRepresenter) tweet.from_hash(params[:tweet]) # httpリクエストの場合 # tweet.from_json(request.body.read) # jsonリクエストの場合 if tweet.save render json: tweet, status: :created else render json: tweet.errors.full_messages, status: :unprocessable_entity end end end
XMLをパースしたい場合も、レンダーと同じようにRepresenterにRoar::XML
をincludeし、from_xml
を呼び出します。
class Tweet < ActiveRecord::Base; end module TweetRepresenter include Roar::XML property :id property :content end xml =<<XML <tweet> <id>1</id> <content>Hoge</content> </tweet> XML tweet = Tweet.new tweet.extend(::TweetRepresenter) tweet.from_xml xml # => #<Tweet id: 1, content: "Hoge", created_at: nil, updated_at: nil>
1.5. Decoratorの定義と使い方
パフォーマンスやオブジェクト汚染のためextend
が嫌いな人のために、デコレーターで実行することもできます。次のように
Roar::Decorator
を継承することでデコレーターを定義します。
# app/representers/tweet_representer.rb class TweetRepresenter < Roar::Decorator include Roar::JSON include Roar::Hypermedia property :id property :name # Decorator内の represented はデコレートするモデルを表します。 end
作成したデコレーターでラップします。
# app/controllers/api/tweets_controller.rb class Api::TweetsController < ApplicationController skip_before_action :verify_authenticity_token def show tweet = Tweet.find(params[:id]) decorator = TweetRepresenter.new(tweet) render json: decorator # =>"{\"id\":1,\"content\":\"Hoge\"}" end end
2. Roarでクライアントとサーバーの連携
2.1. 概要
Roarでサーバー側のRailsアプリ(Tweet)とクライアント側のRailsアプリ(Blog)を連携するようにします。
サンプルはroar_test - GitHubにあります。
シナリオとしては、Tweetアプリ(サーバー側)を既に運用しており、新しいBlogアプリ(クライアント側)を立ち上げようと考えていて、Tweetアプリにデータを公開したいというという感じをイメージして作りました。
ER図は次の通りで、クライアント側はartciles
しかないが、artcile
を投稿した時に、合わせてTag付きでTweetも投稿できるみたいなことをしています。
詳細はGitHubを参照してみればいいので、RoarでCRUDをしながら連携する方法の抜粋(かなり雑です)を記載しました。
2.2. showアクション(単一アイテムの取得)
クライアントの詳細画面で、articleとtweetを表示します。
クライアントのコントローラーは次の通りです。
# app/controllers/artcles_controller.rb def show # ローカル(クライアント)DBから取得 @article = Article.find(params[:id]) # Roarでサーバー側からデータを取得 @tweet = ::Json::Tweet::Client.build.show(@article.remote_tweet_id) end
Roarの定義とクライアントのコードです。
Roar::Client
をincludeすることで、get, post, put, deleteメソッドがincludeされます。
build
メソッドでクライアントを作成し、show
メソッドで、サーバー側のRailsにアクセスしています。
レスポンスは、Reprsenter
とClient
で定義されている、id, content, tagsをパースし、OpenStructの値として代入します。
このとき、as
オプションを使うことで、Tweet.id を Article.remote_tweet_id に変換しています。
# app/representer/json/tweet.rb module Json class Tweet < OpenStruct module Representer include Roar::JSON collection_representer class: ::Json::Tweet property :id property :content collection :tags, class: ::Json::Tag, extend: ::Json::Tag::Representer end # only client side module Client include Roar::JSON include Representer include Roar::Client # ServerからClientへ受け取ったときの変換処理 property :remote_tweet_id, as: :id, # Tweet.id => Article.remote_tweet_id に変換 skip_render: true collection :tags, class: ::Json::Tag, extend: ::Json::Tag::Client # Clientの作成メソッド(Singular用) def self.build ::Json::Tweet.new.extend(::Json::Tweet::Client) end # APIのURL def self.api_url "http://localhost:3001/api/tweets" end # リモートのTweetsController#showにアクセス def show(id) get(uri: "#{::Json::Tweet::Client.api_url}/#{id}", as: 'application/json') end end end end
サーバー側では、単純にServerをextendしているだけです。
# app/controllers/api/tweets_controller.rb def show @tweet = Tweet.find(params[:id]) render json: @tweet.extend(::Json::Tweet::Server) # { # "id": 1, # "content": "tweet 1", # "tags": [ # { "id": 1000, "name": "tag 1" }, # { "id": 1001, "name": "tag 2" } # ] # } end
サーバー側のRepresenterです。
module Json class Tweet < OpenStruct module Representer include Roar::JSON collection_representer class: ::Json::Tweet property :id property :content collection :tags, class: ::Json::Tag, extend: ::Json::Tag::Representer end module Server include Roar::JSON include Representer collection :tags, class: ::Tag, extend: ::Json::Tag::Server, parse_strategy: :find_or_instantiate end end end
2.3. indexアクション(複数アイテムの取得)
コレクションの取得の場合、コレクション用のクライアントを作成し、一覧を取得します。
# app/controllers/articls_controller.rb def index @articles = Article.all # コレクション用のクライアントを作成し、allメソッドで一覧を取得 @tweets = ::Json::Tweet::Client.build_collection.all end
コレクションを取得するには、配列をextendします。また、Representer.for_collection
をextendする必要が有ります。
all
メソッドは、サーバー側のTweetsController#indexアクションにアクセスします。
# app/representers/json/tweet.rb module Json class Tweet < OpenStruct ... # only client side module Client ... # Clientの作成メソッド(Collection用) def self.build_collection [].extend(::Json::Tweet::Client).extend(::Json::Tweet::Representer.for_collection) end # リモートのTweetsController#indexにアクセス def all get(uri: ::Json::Tweet::Client.api_url, as: 'application/json') end end end end
サーバーのコントローラーでもfor_collection
を使って、コレクションを返すようにしています。
>|ruby
def index
@tweets = Tweet.all
render json: @tweets.extend(::Json::Tweet::Server.for_collection)
# [
# {
# "id": 1,
# "content": "tweet 1",
# "tags": [
# { "id": 1000, "name": "tag 1" },
# { "id": 1001, "name": "tag 2" }
# ]
# },
# {
# "id": 2,
# "content": "tweet 2",
# "tags": []
# }
# ]
end
|
2.4. create, update, destroyアクション(アイテムの作成、更新、削除)
コレクションを作成し、作成、更新、削除を行います。
# app/controllers/articles_controller.rb # POST /articles def create @article = Article.new(article_params) @tweet = ::Json::Tweet::Client.build.from_hash(params[:article]) @tweet.create @article.remote_tweet_id = @tweet.remote_tweet_id if @article.save redirect_to @article, notice: 'Article was successfully created.' else render :new end end # PATCH/PUT /articles/1 def update @article = Article.find(params[:id]) @tweet = ::Json::Tweet::Client.build.from_hash(params[:article]) @tweet.update(@article.remote_tweet_id) if @article.update(article_params) redirect_to @article, notice: 'Article was successfully updated.' else render :edit end end # DELETE /articles/1 def destroy @article = Article.find(params[:id]) @tweet = ::Json::Tweet::Client.build @tweet.destroy(@article.remote_tweet_id) @article.destroy redirect_to articles_url, notice: 'Article was successfully destroyed.' end
2.5. 多対多関連のCUD
TweetとTagは多対多関係です。
まず、クライアントサイドでは次のようにして、リクエストを送ります。
# 画面から次のようなパラメータがフォームから送られてきます。 { "utf8"=>"✓", "authenticity_token"=>"xxx", "article" => { "title" => "article 1", "content" => "client article", "tags" => [{ "id" => "1000" }, { "id" => "1001" }, { "id" => "", "name" => "new tag" }] }, "commit"=>"Update Article", "id"=>"1" } # コントローラーで画面のフォーム情報をfrom_hashでパースして取得します # createメソッドでリクエストを送ります。 def create ... @tweet = ::Json::Tweet::Client.build.from_hash(params[:article]) @tweet.create ... end # from_hashのパース時に取得されるデータは次のように定義しています。 module Json class Tweet < OpenStruct module Representer include Roar::JSON collection_representer class: ::Json::Tweet property :id property :content collection :tags, class: ::Json::Tag, extend: ::Json::Tag::Representer end module Client include Roar::JSON include Representer include Roar::Client # ClientからServerへのリクエストを送るときの変換処理 property :title, as: :content, # Article.title => Tag.content 用にキー名を変換 render_filter: -> (value, _doc, _args) { value.to_s[0, 10] + '...' } # Twitter用に文字列を短くする collection :tags, class: ::Json::Tag, extend: ::Json::Tag::Client end end end module Json class Tag < OpenStruct module Representer include Roar::JSON collection_representer class: ::Json::Tag property :id property :name end # only client side module Client include Roar::JSON include Representer include Roar::Client end end end
サーバーサイドでは次の通りです。
# POSTされるjsonデータは次のようになりmす {"id"=>"1", "content"=>"client art...", "tags"=>[{"id"=>"1000"}, {"id"=>"1001"}, {"id"=>"", "name"=>"new tag"}] } # controllerで取得し、from_jsonでパースし、値を設定し保存 tweet = Tweet.new.extend(::Json::Tweet::Server).from_json(request.body.read) tweet.save # パースの内容はRepresenterで定義 # parse_strategy: :find_or_instantiate はidが既にあればそのインスタンスを返し、 # idがなければ新しいインスタンスを作成する module Server include Roar::JSON include Representer collection :tags, class: ::Tag, extend: ::Json::Tag::Server, parse_strategy: :find_or_instantiate end
以上です。