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 使用できるようにしているのか、全く掴めない・・・
力つきました。

そんなわけで、

  • Twitter メンテ時の timeline の JSON が不明。
  • Twitter::Status がどうやって each 使えるようになっているのか?

という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"]

*1:ぼーっとながめる時は termtter と夏ライオン