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で叩く時に、パラメータを配列で渡したいと思ったからです。

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

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

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

これで実行すると、

1
api/foo?piyo1=hoge1&piyo2=hoge2

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

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

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

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

active_resource/base.rb

1
2
3
4
5
6
7
8
9
10
11
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メソッドは以下です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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で渡しているので、

1
2
3
4
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()メソッドが何をしているか見ます。

1
2
3
4
# 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で試してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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は便利なメソッドで引数を渡してあげると配列からクエリ形式に組み立ててくれます。

1
2
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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

1
2
3
4
5
6
7
8
require 'active_resource'

class Foo < ActiveResource::Base

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

end

/app/controllers/foo_controller.rb

1
2
3
4
5
6
7
8
9
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のソースの内訳は以下のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ 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

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

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

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

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

このエントリーをはてなブックマークに追加