ActiveResource の使い方(後編) : 一般の Web API にアクセスする
だいぶ前の話になりますが、 Ruby の「ActiveResource の使い方」という記事を掲載しました。そのとき、 3 回構成の最後で任意の Web API にアクセスする方法をご紹介すると書いたのですが、いろいろなことに時間を取られて放ったらかしになっていました。これではいかん、ということで、本日はその後編を書いてみたいと思います。
参考までに、これまでに掲載した ActiveResource 関連の記事は以下のとおりです。とくに ActiveResource の基礎知識を前編、中編で説明していますので、ご参照ください。
題材
漠然とカスタマイズ方法を書くのはなかなか難しいので、具体的な Web API を題材にして、そのためのカスタマイズ方法を順を追ってご紹介しようと思います。ここでは、「ActiveResource で Google Spreadsheets Data API にアクセスする」の GoogleSpreadsheets::List クラスとほぼ同じ機能を持つ ActiveResource モデル、 GoogleSpreadsheet クラスを作ってみようと思います。同じと言っても、実装は説明しやすいように簡略化してあります。完成版のソースは以下にあります。
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.site や self.prefix は Google Spreadsheets の仕様に合わせて定義しています。他の Web API にアクセスする際は適宜変更してください。また、 self.format に Format クラスのインスタンスを設定することで、 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 を返し、 encode と decode で適切なフォーマット変換を行えば、 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 にアクセスする際は、ぜひこの方法を試してみてください。
詳しくはこちらの記事をどうぞ!
この記事にコメントする