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

rest-client

rest-client/rest-client
rest-client - Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions.

まずはサクッと

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

class ApiClient
  ENDPOINT = 'https://api.example.com/items.json'.freeze

  class << self
    def items(search)
      url = ENDPOINT
      params = {s: search}
      get(url, params)
    end

    def item
      # ...
    end

    private

    def get(url, params)
      res = RestClient.get(url, params: params)
      return nil if res.code != 200

      JSON.parse(res.body)
    end
  end
end

# こんな感じで使う
ApiClient.items('けんさく') => [...]

はい、これだけ。

レスポンスを工夫する

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

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

def items(search)
  url = ENDPOINT
  params = {s: search}
  get(url, params)
  return [] if data.nil?

  data['items'].map do |item|
    ItemResponse.new(item)
  end
end

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

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

hash = {hoge: 'hogehoge', fuga: 'fugafuga', hash: { key1: 'value1', key2: 'value2'}}
obj = ApiClient::Response.new(hash)

obj.hoge => 'hogehoge'
obj.fuga => 'fugafuga'
obj.hash => {:key1=>"value1", :key2=>"value2"}

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

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

payjp/payjp-ruby
Contribute to payjp-ruby development by creating an account on Github.

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

class ApiClient
  # (略)

  class Response
    def initialize(hash)
      @values = {}
      instance_eval do
        add_accessors(hash.keys)
      end

      hash.each do |k, v|
        @values[k] = v
      end
    end

    private

    def add_accessors(keys)
      metaclass.instance_eval do
        keys.each do |k|
          k_eq = :"#{k}="
          define_method(k) { @values[k] }
          define_method(k_eq) do |v|
            if v == ""
              raise ArgumentError.new(
                      "You cannot set #{k} to an empty string." \
                      "We interpret empty strings as nil in requests." \
                      "You may set #{self}.#{k} = nil to delete the property.")
            end
            @values[k] = v
          end
        end
      end
    end

    def metaclass
      class << self; self; end
    end
  end

  class ItemResponse < Response
    # 必要であればメソッド生やす
    def my_method
    end
  end
end

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

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

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

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

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

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