WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

ActiveResource の使い方(後編) : 一般の Web API にアクセスする

だいぶ前の話になりますが、 Ruby の「ActiveResource の使い方」という記事を掲載しました。そのとき、 3 回構成の最後で任意の Web API にアクセスする方法をご紹介すると書いたのですが、いろいろなことに時間を取られて放ったらかしになっていました。これではいかん、ということで、本日はその後編を書いてみたいと思います。

参考までに、これまでに掲載した ActiveResource 関連の記事は以下のとおりです。とくに ActiveResource の基礎知識を前編、中編で説明していますので、ご参照ください。

題材

漠然とカスタマイズ方法を書くのはなかなか難しいので、具体的な Web API を題材にして、そのためのカスタマイズ方法を順を追ってご紹介しようと思います。ここでは、「ActiveResource で Google Spreadsheets Data API にアクセスする」の GoogleSpreadsheets::List クラスとほぼ同じ機能を持つ ActiveResource モデル、 GoogleSpreadsheet クラスを作ってみようと思います。同じと言っても、実装は説明しやすいように簡略化してあります。完成版のソースは以下にあります。

http://webos-goodies.googlecode.com/svn/trunk/blog/articles/how_to_use_activeresource_3/gspreadsheets_resource.rb

GoogleSpreadsheets::List の仕様などは、上記の記事で説明していますので、そちらをご参照ください。手抜きですいません(^^ゞ

共通部分の実装

まずはカスタマイズした ActiveResource モデルを定義するときに必須のコードを記述しました。他の Web API にアクセスするモデルを作成する際も、このコードをテンプレートにすることができると思います。

require 'rubygems'
require 'active_support'
require 'active_resource'
require 'time'
require 'erb'
 
class GoogleSpreadsheet < ActiveResource::Base
 
  class Connection < ActiveResource::Connection
  end
 
  class Format
    def extension() '' end
    def mime_type() 'application/atom+xml' end
    def encode(hash, options = {}) hash.to_xml(options) end
    def decode(xml)
      data = Hash.from_xml(xml)
      data.is_a?(Hash) && data.keys.size == 1 ? data.values.first : data
    end
  end
 
  #-------------------------------------------------------------------
 
  self.site   = 'http://spreadsheets.google.com/'
  self.prefix = '/:document_id/:worksheet_id/:visibility/:projection/'
  self.format = Format.new
 
  class << self
    def connection(refresh = false)
      if defined?(@connection) || self == GoogleSpreadsheet
        @connection = Connection.new(site, format) if refresh || @connection.nil?
        @connection.user = user if user
        @connection.password = password if password
        @connection.timeout = timeout if timeout
        @connection
      else
        superclass.connection
      end
    end
  end
 
end

Connection クラスは Web API にアクセスするための HTTP コネクションを抽象化したクラスで、 ActiveResource::Connection を継承しています。このクラスで各種メソッドをオーバーライドすることで、 Web API へのアクセス方法をカスタマイズできます。

Format クラスは Web API からのレスポンスと ActiveResource が扱える形式との間の変換などを行うクラスです。 ActiveResource 本体のコードではモジュールで実装されているのですが、クラスとして実装した方が継承などもできて便利なので、私はそうしています。定義している各メソッドの詳細は「データフォーマットのカスタマイズ」を参照してください。

GoogleSpreadsheets.Connection メソッドは、 ActiveResource が内部でコネクションが必要なときに呼ばれるものです。これを再定義することで、カスタマイズした Connection インスタンスを使うようにしています。メソッドの中身は ActiveResource::Base.Connection の実装をほぼそのままパクっています。

その他、 self.siteself.prefix は Google Spreadsheets の仕様に合わせて定義しています。他の Web API にアクセスする際は適宜変更してください。また、 self.formatFormat クラスのインスタンスを設定することで、 ActiveResource がカスタマイズしたフォーマットを利用するようになります。

それでは、このコードをベースとして、 Google Spreadsheets にアクセスするためのカスタマイズを施していきましょう。

ユーザー認証のカスタマイズ

前編でもご紹介したとおり、 ActiveResource はユーザー認証として BASIC 認証を標準でサポートしています。しかし、最近の Web API ではセキュリティー上の理由などから BASIC 認証を使っているものはほとんどありません。 Google Spreadsheets API も例外ではなく、認証には AuthSub, OAuth, ClientLogin のいずれかの方式を使うことになっています。ここでは、ユーザー名とパスワードを直接指定することで認証する ClientLogin の方式で実装することにします。

ClientLogin の認証方式では、まず特定の URL にユーザー名とパスワードを送信し、アクセストークンを取得します。そして、実際に API を呼び出す際に、そのトークンを Authorization リクエストヘッダに指定することで、認証を行います。詳細は ClientLogin のドキュメントをご参照ください。

認証関連のリクエストヘッダの設定は、 Connection クラスの authorization_header メソッドで行うのが良いと思います。ここでは、アクセストークンの取得自体もその中で行うようにします。実際のコードは以下のようになります。

class Connection < ActiveResource::Connection
  def authorization_header
    if @user && @password && !@token
      email            = ERB::Util.u(@user)
      password         = ERB::Util.u(@password)
      http             = Net::HTTP.new('www.google.com', 443)
      http.use_ssl     = true
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
      resp, data = http.post('/accounts/ClientLogin',
                             "accountType=HOSTED_OR_GOOGLE&Email=#{email}&Passwd=#{password}&service=wise",
                             { 'Content-Type' => 'application/x-www-form-urlencoded' })
      handle_response(resp)
      @token = (data || resp.body)[/Auth=(.*)/n, 1]
    end
    @token ? { 'Authorization' => "GoogleLogin auth=#{@token}" } : {}
  end
end

この authorization_header メソッドは、 Web API へのリクエストが必要になるたびに、 ActiveResource から呼ばれます。そこで、(もしまだトークンを取得していなければ) ClientLogin の認証用の URL にアクセスしてアクセストークンを取得し、それをリクエストヘッダに設定しています(返り値のハッシュがそのままリクエストヘッダに設定されます)。

データフォーマットのカスタマイズ

Google Spreadsheets API が扱うデータフォーマット (GData) は ActiveResource 標準のフォーマットとはだいぶ違いますので、 Format クラスをカスタマイズして形式を変換してやらなくてはなりません。カスタマイズ後の Format クラスは以下になります。

class Format
 
  def extension() '' end
  def mime_type() 'application/atom+xml' end
 
  def encode(hash, options = {})
    root = REXML::Element.new('entry')
    root.add_namespace('http://www.w3.org/2005/Atom')
    root.add_namespace('gsx', 'http://schemas.google.com/spreadsheets/2006/extended')
    hash.each do |key, value|
      if value && (key = key.dup).gsub!(/^gsx_/u, 'gsx:')
        e = REXML::Element.new(key, root)
        e.text = value
      end
    end
    root.to_s
  end
 
  def decode(xml)
    e = Hash.from_xml(xml.gsub(/<(\/?)gsx:/u, '<\1gsx_'))
    if e.has_key?('feed')
      e = e['feed']['entry'] || []
      (e.is_a?(Array) ? e : [e]).each{|i| format_entry(i) }
    else
      format_entry(e['entry'])
    end
  end
 
  private
 
  def format_entry(e)
    e['id']      = e['id'][/[^\/]+\z/u] if e.has_key?('id')
    e['updated'] = (Time.xmlschema(e['updated']) rescue Time.parse(e['updated'])) if e.has_key?('updated')
    e
  end
 
end

まずは Format クラスのメソッドを軽く説明しておきましょう。

extension()
リクエストする URL の拡張子を返します。しかし、後で URL そのものをカスタマイズしますので、少なくとも今回の GoogleSpreadsheets クラスでは呼び出されることはありません。
mime_type()
Web API がやりとりするデータフォーマットの MIME Type を返します。 GET / DELETE メソッドでの Accept リクエストヘッダ、 PUT / POST メソッドでの Content-Type リクエストヘッダに指定されます。 GData は ATOM の拡張なので、ここでは 'application/atom+xml' を指定しています。
encode(hash, options)
渡された hash の内容を、 POST / PUT のリクエストボディーに指定できる文字列に変換します。第二引数は Hash::to_xml メソッドに渡す引数です。 Hash::to_xml を使わないのなら無視してもかまいません。
decode(response_body)
渡された response_body (Web API が返したレスポンスボディーそのものの文字列)を、 ActiveResource が扱える Hash (基本的にフィールド名 ⇒ 値 の集合)に変換します。

ということで、基本的には mime_type でデータフォーマットの MIME Type を返し、 encodedecode で適切なフォーマット変換を行えば、 ActiveResource で任意のデータフォーマットが扱えるようになります。ただし、 2 つほど注意点があります。

まず、 encode / decode メソッドには、そのデータをどの URL でから取得したか、もしくはどの URL に送信するのかという情報は渡されないことです。とくに、そのデータが find(:all) などによって取得されたコレクションなのか、それとも find(id) などによる単体データなのかすらわからないのは苦しいところです。 Google Spreadsheets API では幸運にも、ルートタグによってコレクション (<feed>) か単体データ (<entry>) かが判別できましたが、他の API では問題になるかもしれません。

ふたつめは、 decode メソッドが返す Hash が階層構造になっている場合です。たとえば、仮に GoogleSpreadsheets::Format#decode メソッドが以下のような Hash を返したとします。

{
  "id" => "abcdef"
  "gsx_field" => "foo",
  "link" => [
    {
      "rel" => "self",
      "type" => "application/atom+xml",
      "href" => "http://spreadsheets.google.com/feeds/spreadsheets/private/full/key"
    }
  ]
}

ここから生成されるモデルインスタンス(GoogleSpreadsheets クラスのインスタンス)の attributes は以下の構造になります。

{
  "id" => "abcdef",
  "gsx_field" => "foo",
  "link" => [
    GoogleSpreadsheets::Link.new({
      "rel" => "self",
      "type" => "application/atom+xml",
      "href" => "http://spreadsheets.google.com/feeds/spreadsheets/private/full/key"
    })
  ]
}

"link" の内容が GoogleSpreadsheets::Link インスタンスになっていることに注目してください。このように、 decode メソッドが返す Hash の要素に Hash が格納されていると、モデルクラス(GoogleSpreadsheets クラス)内にその要素のキーを camelize した名前で ActiveResource::Base のサブクラスを自動的に定義し、 Hash をそのクラスのインスタンスで置き換えます。当然、 encode メソッドにも Hash がサブクラスのインスタンスで置き換えられたものが渡されるので、それを処理できるように実装する必要があります。

まあ、 decode メソッドがフラットな構造の Hash を返すようにするのが、もっとも無難だとは思います。

アクセス URL のカスタマイズ

最後に、 ActiveResource がアクセスする URL をカスタマイズします。前編で説明したとおり、 ActiveResource はデフォルトで "http://サイト/コレクション名/プリフィクス/ID.拡張子" のような URL にリクエストを送信します。しかし、アクセスポイントがこれにピッタリあてはまるのは稀でしょうから、やはりカスタマイズが必要になります。

具体的には、 ActiveResource::Base.element_path, ActiveResource::Base.collection_path の 2 つのメソッドをオーバーライドします。 GoogleSpreadsheets クラスでは、以下のようにしています。

class GoogleSpreadsheet < ActiveResource::Base
  # 省略...
 
  class << self
    def connection(refresh = false)
      # 省略...
    end
    def element_path(id, prefix_options = {}, query_options = nil)
      prefix_options, query_options = split_options(prefix_options) if query_options.nil?
      "/feeds/list#{prefix(prefix_options)}#{id}#{query_string(query_options)}"
    end
    def collection_path(prefix_options = {}, query_options = nil)
      prefix_options, query_options = split_options(prefix_options) if query_options.nil?
      "/feeds/list#{prefix(prefix_options)}#{query_string(query_options)}"
    end
  end
 
end

element_path は単体データのアクセス(find(id) やデータの保存・削除など)に使われる URL を、 collection_path はコレクションのアクセス(find(:all) など)に使われる URL を、それぞれ定義します。それぞれのメソッド内で、 prefix(prefix_options) はパラメータ置換後のプレフィクス、 query_string(query_options) はプレフィクスパラメータを除いたパラメータをクエリーパラメータ (?name=value&…) の形式で並べたものに展開されます。その他、もちろん collection_name や id などのメソッドも使えます。

通常の REST 形式の API であればこれだけで OK なのですが、 Google Spreadsheets API ではデータを編集・削除するための URL がちょっと特殊です。データを取得した際の link 要素に編集用の URL が設定されているので、その URL に対してリクエストを送信する必要があります。これは ActiveResource の標準の枠組みでは対応できないので、仕方なく update, destroy メソッドをオーバーライドすることにしました。

class GoogleSpreadsheet < ActiveResource::Base
  # 省略...
 
  def destroy() connection.delete(edit_path, self.class.headers) end
 
  protected
 
  def update
    returning connection.put(edit_path, encode, self.class.headers) do |response|
      load_attributes_from_response(response)
    end
  end
  def edit_path()
    s = self.class.site
    (self.attributes['link'] || []).map{|l| l.rel == 'edit' ? l.href : nil }.compact.each do |href|
      e = URI.parse(href)
      return e.request_uri if s.scheme == e.scheme && s.port == e.port && s.host == e.host
    end
    raise 'No edit link'
  end
 
end

データを取得した際に link 要素の内容も attributes に保存しておき、 update / destroy が呼び出された際にそこから編集用の URL を取り出し、リクエスト URL を差し替えています。このような構造のため、 GoogleSpreadsheets.delete メソッドは機能しません(データを取得しないと編集用 URL がわからないので)。

実行してみる

以上で、 Google Spreadsheets API に合わせたカスタマイズは完了です。それでは、試しに実行してみましょう。ここでは、以下のようなスプレッドシートから県名のカラムを抜き出して表示することにします。

スクリプトは以下のようになります。

#! /usr/bin/ruby
 
require 'gspreadsheets_resource'
 
GoogleSpreadsheet.user     = 'ユーザー名'
GoogleSpreadsheet.password = 'パスワード'
 
params = {
  :document_id => '0Ao0lgngMECUtdGVrNDdqeVhudUFGdzRZeGpOdEtFb3c',
  :worksheet_id => 'od6',
  :visibility => 'private',
  :projection => 'full'
}
 
rows = GoogleSpreadsheet.find(:all, :params => params)
 
rows.each do |row|
  puts "#{row.attributes['gsx_県名']}"
end

これですべての県名が表示されるはずです。詳しい使い方はこちらのページの GoogleSpreadsheets::List クラスを参照してください。

通信内容のダンプ

最後に、デバッグ時の Tips をひとつ。 Ruby の Net::HTTP クラスには、すべての通信内容をダンプできるという、とても便利な機能があります。もちろん ActiveResource も Net::HTTP クラスを使っていますので、その恩恵にあずかることができます。それには、 Connection クラスに以下のコードを追加してください。

class Connection < ActiveResource::Connection
  DEBUG = false
  # 省略...
  private
  def http() http = super; http.set_debug_output($stderr) if DEBUG; http end
end

こうして、 DEBUG 定数を true に変更して実行すれば、通信内容が標準エラー出力に表示されます。 set_debug_output に渡している引数を変えれば、ファイルにログを取ることもできますので、都合に合わせて変更してください。

以上、本日は特定の Web API にアクセスするように ActiveResource をカスタマイズする方法を、 Google Spreadsheets API を例にしてご紹介しました。もともと Rails アプリ間の通信のために設計されているので、素直に実装できない部分もありますが、それでもいちからライブラリを作るよりらくだと思います。また、以前の記事でも書きましたが、ライブラリの使い方が統一できるのも魅力です。 Ruby から Web API にアクセスする際は、ぜひこの方法を試してみてください。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles