「AWS無料相談会」をオンラインで開催中

ActiveResourceでGET送信時に配列で渡す

先日社内の「業務効率化周辺グッズ購入補助」制度を利用して、mac用にモバイル充電器HyperJuiceを購入し、配線地獄になっている前田です。

本日はRuby On RailsのGem、AcitiveResourceを触ってみたので、そのお話をします。

環境
Rails 4.1.5、ruby 2.1.2、activeresource-4.0.0

ActiveResorceとは「Web上のRESTfulAPIをActiveRecordのモデルと同じようなインターフェースで利用可能にする」というGemライブラリです。
webAPIをActiveResourceと同じように操作出来たら便利ですね。  

今回やりたいと思ったのは、下記のようにGETで叩く時に、パラメータを配列で渡したいと思ったからです。

http://hoge/api/foo?piyo[]=hoge1&piyo[]=hoge2  

active_resourceでパラメータを渡す時は、下記のように設定すると思います。

Foo.find(:all, from:'api/foo', params: { piyo1: :hoge1, piyo2: :hoge2 } )  

これで実行すると、

api/foo?piyo1=hoge1&piyo2=hoge2  

というGETパラメータが出来上がります。
しかし、配列形式で渡したいからといって下記のように渡すと、

Foo.find(:all, from:'api/foo', params: { piyo[]: :hoge1, piyo[]: :hoge2 } )  

エラーになってしまいます。
今回はこれを解決していきたいと思います。

では、ActiveResourceの中の処理を順番に見ていきます。
findメソッドを定義しているのはactive_resource/base.rbです。

active_resource/base.rb

  def find(*arguments)
    scope   = arguments.slice!(0)
    options = arguments.slice!(0) || {}
     case scope
      when :all   then find_every(options)
      when :first then find_every(options).first
      when :last  then find_every(options).last
      when :one   then find_one(options)
      else             find_single(scope, options)
    end
  end

858行目ですね。
検索はここが起点になります。

第一引数を:allで渡すので、
when :all then find_every(options)
の処理になります。

そしてfind_everyメソッドは以下です。

  # Find every resource
  def find_every(options)
    begin
      case from = options[:from]
      when Symbol
        instantiate_collection(get(from, options[:params]), options[:params])
      when String
        path = "#{from}#{query_string(options[:params])}"
        instantiate_collection(format.decode(connection.get(path, headers).body) || [], options[:params])
      else
        prefix_options, query_options = split_options(options[:params])
        path = collection_path(prefix_options, query_options)
        instantiate_collection( (format.decode(connection.get(path, headers).body) || []), query_options, prefix_options )
      end
    rescue ActiveResource::ResourceNotFound
      # Swallowing ResourceNotFound exceptions and return nil - as per
      # ActiveRecord.
      nil
    end
  end

今回は
find(:all, from:'api/com', params: params )
とfromをStringで渡しているので、

  when String
    path = "#{from}#{query_string(options[:params])}"
    instantiate_collection(format.decode(connection.get(path, headers).body) || [], options[:params])
  else

の中の処理になります。

ここでpathを生成しますが、先ほど渡した from:'api/foo', params: params からpathを生成しています。
fromのほうはいいですが、:params のほうですね。

query_string()メソッドが何をしているか見ます。

  # Builds the query string for the request.
  def query_string(options)
    "?#{options.to_query}" unless options.nil? || options.empty?
  end

渡したoptionsに、to_queryをしているだけですね。
ここがエラーの原因です。
rails consoleで試してみます。

# Hashで渡す
2.1.2 :021 > {piyo1: :hoge1, piyo2: :hoge2}.to_query
 => "piyo1=hoge1&piyo2=hoge2"

# Hashに[]を付けて渡す
2.1.2 :055 > {piyo1[]: :hoge1, piyo2[]: :hoge2}.to_query
SyntaxError: (irb):55: syntax error, unexpected ':', expecting =>

# エスケープできないか試す
2.1.2 :057 >   {piyo1[]: :hoge1, piyo2[]: :hoge2}.to_query
SyntaxError: (irb):57: syntax error, unexpected $undefined, expecting keyword_do or '{' or '('

# Arrayの場合
2.1.2 :003 >   ['hoge1', 'hoge2'].to_query
ArgumentError: wrong number of arguments (0 for 1)

# Stringの場合
2.1.2 :001 > 'hoge1, hoge2'.to_query
ArgumentError: wrong number of arguments (0 for 1)

なので、ここの処理にパッチをあてます。

実はto_queryは便利なメソッドで引数を渡してあげると配列からクエリ形式に組み立ててくれます。

2.1.2 :006 > ['hoge1', 'hoge2'].to_query('piyo')
 => "piyo%5B%5D=hoge1&piyo%5B%5D=hoge2"

%5B%5Dは[]のエスケープです。
実際のURLを叩く時にはpiyo[]=hoge1&piyo[]=hoge2  
と同じ動作をします。

なので、結論としては下記のようにオーバーライドします。

/app/model/foo.rb

require 'active_resource'

class Foo < ActiveResource::Base

  self.site = 'http://hoge'

  def self.query_string(options)
    "?#{options.to_query('piyo')}" unless options.nil? || options.empty?
  end
end

このままだとこのクラスのみのオーバーライドで、これでも良いと思いますが、もう少し凡庸化してプロジェクト全体で使えるようにパッチをあててみます。

/config/initializers/active_resource_pach.rb

require 'active_resource'
class ActiveResource::Base
  class << self
    def query_string(options)
      if tag && options.instance_of?(Array)
        "?#{options.to_query(tag)}" unless options.nil? || options.empty?
      elsif options.instance_of?(Hash)
        "?#{options.to_query}" unless options.nil? || options.empty?
      elsif options.instance_of?(String)
        "?#{options}" unless options.blank?
      else
        raise ArgumentError
      end
    end

    def tag
      return @tag if defined?(@tag)
      nil
    end

    def tag=(tag)
      @tag = tag
    end
  end
end

Hashでの処理は今までどおりにしています。
query_stringメソッドはbase.rbのプライベートメソッドで、base.rbの中で使用されている別の箇所を見てみましたが、上記の実装で影響は無いと思います。

/app/models/foo.rb

require 'active_resource'

class Foo < ActiveResource::Base

  self.site = 'http://hoge'
  self.tag = 'piyo'

end

/app/controllers/foo_controller.rb

class FooController < ApplicationController

  def index
    @foo = Foo.find(:all, from: '/api/foo', params: ['hoge1', 'hoge2'])
    render :template => "foo/index"
  rescue => e
    render :text => e.message
  end
end

こんな感じで外のメソッドにもパッチを当てていけますね。
パッチだらけだと、ActiveResourceを使う意味があまり無くなりそうですが。

今回取り上げたActiveResourceのソースの内訳は以下のようになっています。

$ activeresource-4.0.0
├── README.rdoc
└── lib
    ├── active_resource
    │   ├── associations
    │   │   └── builder
    │   │       ├── association.rb
    │   │       ├── belongs_to.rb
    │   │       ├── has_many.rb
    │   │       └── has_one.rb
    │   ├── associations.rb
    │   ├── base.rb
    │   ├── callbacks.rb
    │   ├── collection.rb
    │   ├── connection.rb
    │   ├── custom_methods.rb
    │   ├── exceptions.rb
    │   ├── formats
    │   │   ├── json_format.rb
    │   │   └── xml_format.rb
    │   ├── formats.rb
    │   ├── http_mock.rb
    │   ├── log_subscriber.rb
    │   ├── observing.rb
    │   ├── railtie.rb
    │   ├── reflection.rb
    │   ├── schema.rb
    │   ├── singleton.rb
    │   ├── validations.rb
    │   └── version.rb
    └── active_resource.rb

ActiveResourceはファイルが少ないのですごく見やすく、rails初心者の勉強にはちょうど良さそうですね!

目次

PS

ブログを書いてしまった後で気付いたのですが、

2.1.2 :060 > {'piyo1[]' => 'hoge1', 'piyo[]' => 'hoge2'}.to_query
 => "piyo%5B%5D=hoge2&piyo1%5B%5D=hoge1"

こうすると渡せました...
まぁでもちょっと使いやすくなったと思いますので良しとして下さい...

Ruby on Railsを活用したWebサービスや業務システム開発をご検討の企業様は、是非MMMにご相談下さいませ!