Twitter4rのエラーを追って見る
普段、twitter には twitter4r を使った投稿用のスクリプトで投げている。仕事中にぼやきたい時などにターミナルから直接投げれるのでちょっと便利*1。
そんなわけでタイムラインはあまり見ていない。投稿時に10行だけ表示されるようにしているので、チラっと見るくらい。
さて、先日たまたま定期メンテ時に投稿しようとしたらエラー吐き出したので、何が起こっているのかソースを追ってみた記録。
いきなりのエラー
/ruby/gems/1.8/gems/twitter4r-0.3.0/lib/twitter/client/timeline.rb:68:in `timeline_for': undefined method `each' for :Twitter::Status (NoMethodError)
さっきまで普通に投げられたのに、突然壊れたかと思ったら、Twitterがメンテ中だった。
メンテだとしても、もう少し気の利いたエラーを出して欲しいものだと思いつつ、ちょっと twitter4r ソースを見てみた。
twitter4r
エラー箇所のソース @ twitter4r
# /twitter4r-0.3.0/lib/twitter/timeline.rb def timeline_for(type, options = {}, &block) raise ArgumentError, "Invalid timeline type: #{type}" unless @@TIMELINE_URIS.keys.member?(type) uri = @@TIMELINE_URIS[type] response = http_connect {|conn| create_http_get_request(uri, options) } timeline = Twitter::Status.unmarshal(response.body) timeline.each {|status| bless_model(status); yield status if block_given? } timeline end
メンテ中でもHTTPステータスコードは200らしい。http_connection はresponseを返す。
# /twitter4r-0.3.0/lib/twitter/model.rb def unmarshal(raw) input = JSON.parse(raw) def unmarshal_model(hash) self.new(hash) end return unmarshal_model(input) if input.is_a?(Hash) # singular case result = [] input.each do |hash| model = unmarshal_model(hash) if hash.is_a?(Hash) result << model end if input.is_a?(Array) result # plural case end
Twitter::Status.unmarshal(response.body) は、通常JSONをパースした Hash から生成された Twitter::Status のインスタンスを返す。
このインスタンスの中身が nil になってしまうわけか。
response.body に JSON ではなく login 辺りが返されてるとして、JSON.parse は一体何を返してくるんだ?
json
gem の依存で json-1.1.3 が入っていることと、twitter.rb で
require 'json'
とかなってるので gem を使っていると解釈。
parser.rb のソースは読んでみたが、ひたすら case 文とかそんな感じなので省略。
実行結果。
JSON.parse('{"name": "hoge"}') #=> {"name"=>"hoge"} JSON.parse('{}') #=> {} JSON.parse('["foo","bar"]') #=> ["foo", "bar"] JSON.parse('[]') #=> []
json gem は大変お行儀がいいのか、ちゃんと例外出してくれます。わかりやすい。
ここでエラーが吐かれていないので、やはり Hash か Array を返しているのだろう。
Twitter::Status
では、nil っぽいものを返してくる Twitter::Status を見てみる。
Hash を受け取って self.new してるわけだが、コンストラクタ定義がない。
class Status include ModelMixin @@ATTRIBUTES = [:id, :text, :created_at, :user] ...
引数受け取れないんじゃね?と思っていたら、include している ModelMixin が include している ClassUtilMixin に initialize が定義されていた。
module ClassUtilMixin #:nodoc: def self.included(base) #:nodoc: base.send(:include, InstanceMethods) end # Instance methods defined for <tt>Twitter::ModelMixin</tt> module. module InstanceMethods #:nodoc: (中略) def initialize(params = {}) params.each do |key,val| self.send("#{key}=", val) if self.respond_to? key end self.send(:init) if self.respond_to? :init end (略)
params内のkey-valueをそのまま対象のクラスに送り込み、インスタンス変数として使用できるようにしている。
APIのパラメター名が attr_accessor にセットされているので、外からアクセス可能なインスタンス変数ができあがる。
投了
で、無事 Twitter::Status.new(hash) なインスタンスが返される。
ここで問題がもう一つ。
timeline.each {|status| bless_model(status); yield status if block_given? }
どこで Enumable 使用できるようにしているのか、全く掴めない・・・
力つきました。
そんなわけで、
という2点を疑問として残し、投了です。
おまけ
#!/usr/bin/env ruby # -*- coding:utf-8 -*- require 'rubygems' require 'twitter' require 'time' twitter = Twitter::Client.new public_timeline = twitter.timeline_for(:public, :count => 1) do |status| p status.methods end #=>["inspect", "tap", "clone", "created_at=", "public_methods", "object_id", "__send__", "text", "instance_variable_defined?", "init", "equal?", "freeze", "to_json", "text=", "extend", "send", "j", "methods", "to_yaml_properties", "hash", "dup", "to_enum", "to_yaml", "taguri", "instance_variables", "require_block", "eql?", "taguri=", "jj", "instance_eval", "id", "to_i", "singleton_methods", "client", "id=", "taint", "user", "frozen?", "instance_variable_get", "enum_for", "client=", "bless", "to_yaml_style", "instance_of?", "display", "to_a", "method", "user=", "type", "instance_exec", "protected_methods", "==", "===", "instance_variable_set", "basic_bless", "respond_to?", "kind_of?", "to_s", "class", "to_hash", "private_methods", "=~", "tainted?", "__id__", "JSON", "untaint", "nil?", "is_a?", "created_at"]