オープンではないサービスのAPIを使うとき、APIクライアントが無いってことが多いです。なのでアプリケーションから使いたいAPIだけを叩くような簡易なAPIクライアントを自分で書きます。そのときはrest-clientあたりを使ってベタっと書いてしまうことがほとんどです。

rest-client

まずはサクッと

例をいきなり書いてしまうと↓な感じでやります。

 1class ApiClient
 2  ENDPOINT = 'https://api.example.com/items.json'.freeze
 3
 4  class << self
 5    def items(search)
 6      url = ENDPOINT
 7      params = {s: search}
 8      get(url, params)
 9    end
10
11    def item
12      # ...
13    end
14
15    private
16
17    def get(url, params)
18      res = RestClient.get(url, params: params)
19      return nil if res.code != 200
20
21      JSON.parse(res.body)
22    end
23  end
24end
25
26# こんな感じで使う
27ApiClient.items('けんさく') => [...]

はい、これだけ。

レスポンスを工夫する

なんですが、レスポンスをJSON.parseしたHashそのままというのはなんだかイケてないので、rest-clietを使っているAPIクライアントライブラリはどうしているのか、みたいなことを調べました。いい感じの実装を発見したので、それをインスパイアしてみました。

まず、Hashの扱いが少し変わります。

 1def items(search)
 2  url = ENDPOINT
 3  params = {s: search}
 4  get(url, params)
 5  return [] if data.nil?
 6
 7  data['items'].map do |item|
 8    ItemResponse.new(item)
 9  end
10end

こうするとApiClient.items('xxx')の結果はItemResponseの配列となります。

で、このItemResponseとは何者かといいますと、レスポンスのHashをJSのオブジェクトっぽいものに変換する(ドットでアクセスできるようにする)ためのクラスです。

1hash = {hoge: 'hogehoge', fuga: 'fugafuga', hash: { key1: 'value1', key2: 'value2'}}
2obj = ApiClient::Response.new(hash)
3
4obj.hoge => 'hogehoge'
5obj.fuga => 'fugafuga'
6obj.hash => {:key1=>"value1", :key2=>"value2"}

実装が半端なので入れ子のHashはメソッドでアクセスできるようになっていませんが、僕のケースはこれで事足りたのでそれ以上はがんばりませんでした。

ちなみに、このResponseクラスの実装はpayjp-rubyのこの辺のコードを参考にしています(実際には参考にしたのはしばらく前なので、そのときから更に実装が変わっているみたい。Hashのネストも対応してそう)。

Responseとそれを継承したItemResponseというのをApiClient内に用意する形にしました。

 1class ApiClient
 2  # (略)
 3
 4  class Response
 5    def initialize(hash)
 6      @values = {}
 7      instance_eval do
 8        add_accessors(hash.keys)
 9      end
10
11      hash.each do |k, v|
12        @values[k] = v
13      end
14    end
15
16    private
17
18    def add_accessors(keys)
19      metaclass.instance_eval do
20        keys.each do |k|
21          k_eq = :"#{k}="
22          define_method(k) { @values[k] }
23          define_method(k_eq) do |v|
24            if v == ""
25              raise ArgumentError.new(
26                      "You cannot set #{k} to an empty string." \
27                      "We interpret empty strings as nil in requests." \
28                      "You may set #{self}.#{k} = nil to delete the property.")
29            end
30            @values[k] = v
31          end
32        end
33      end
34    end
35
36    def metaclass
37      class << self; self; end
38    end
39  end
40
41  class ItemResponse < Response
42    # 必要であればメソッド生やす
43    def my_method
44    end
45  end
46end

プロキシサーバーを経由する

冒頭に「オープンではないサービスのAPI」と書きました。そういう場合って、特定のIPアドレスからしかアクセスできませんみたい制約があったりするので、開発時には工夫が必要です。

このクライアントを実装したときには、プロキシサーバーのIPを許可してもらい、クライアントはプロキシサーバー経由でAPIリクエストを投げるようにしました。

rest-clientの場合のプロキシ設定は非常に簡単で、

1RestClient.proxy = "https://accountname:password@54.xxx.yyy.zzz"

としておくだけでOK。Railsならinitializerで書いておくのが良いです。