at posts/single.html

tDiary の profile.rb を DSL 風に書き直した & RSpec のテストを書いた

昨日の日記からの続き。 tDiary のサイドバーにプロフィールを表示できる profile.rb に、ちょっと手を加えた。

  • RSpec でテストコードを書いた。
  • 内部のリファクタリング。新しいプロフィールサービスを DSL 風に追加できるようにした。
  • Gravatar サービスを追加。オプションで画像サイズを指定できるようにした。

テストコード

まずは tDiary プラグインから独立している、 Service モジュール部分だけのテストコードを書いた。 実際に各サービスにデータを取りに行って、その結果を確認するだけのホワイトボックス的なテスト。 たとえば、 Twitter のテストコードは、以下のようになっている。

  describe "Twitter" do
    before :all do
      # http://twitter.com/tdiary
      @profile = Profile::Service::Twitter.new("tdiary")
    end

    it "should include name, description, image properties" do
      @profile.name.should == "tDiary.org"
      @profile.description.should == "tDiaryオフィシャルアカウント"
      @profile.image.should match(%r{^http://.*\.(png|jpg)$})
    end
  end

tDiary プラグイン側のテスト(データのキャッシュ&HTMLへの変換)は、まだ書いていない…。 スタイルシートの分離など、考えることがまだあるんだよね。

DSL 風のサービス定義

昨日書いたコードは、 GitHub や Twitter などの各サービスで、同じような処理をコピペしていた。

  • id をキーにして API を呼び出し、 XML ファイルを取得する
  • XML ファイルを解析し、画像ファイルや名前などを取得する

これじゃあんまりなので、似た処理を1箇所にまとめる。 まず、ソースコードを眺めたところ、サービスごとの差異は以下の箇所くらいだと気がついた。

  • APIのエンドポイント
  • XML ファイルでの各プロパティの位置

各サービスクラスでは、この違いだけを定義すけばいいはず。 ちょっと考えた結果、 Rails っぽく DSL で書けるようにしてみた。 例えば、 Twitter の場合は以下のように書くだけでいい。

class Twitter < Base
  property :name, '//user/name'
  property :image, '//user/profile_image_url'
  property :description, '//user/description'
  endpoint {|id| "http://twitter.com/users/show/#{id}.xml" }
end

profile = Twitter.new('machu')
puts profile.name
puts profile.image
puts profile.description

このクラスを new すると、 id をキーにして endpoint で指定した URL から XML を取得する。 そして、取得した XML から name, image, description 属性を取り出す。 取り出す場所は property の第2引数で指定している。

はじめて DSL 風なクラスを作ってみたんだけど、思った以上に簡単に作ることができてちょっとビックリ。 property と endpoint は以下のように Base クラスで定義している。

class Base
  # class instance variables
  class << self
    attr_reader :properties
    attr_reader :endpoint_proc
  end

  # set property and xpath pair for parse XML document
  def self.property(property, path)
    @properties ||= {}
    @properties[property] = path
  end

  # set endpoint proc (this proc is called by initialize method with id)
  def self.endpoint(&block)
    @endpoint_proc = block
  end
end

最初はクラス変数で @@endpoint_proc と @@properties を宣言していたんだけど、クラス変数の場合は子クラスでも同じ変数を共有する。 なので、 Twitter で設定した値が、 GitHub で上書きされてしまったりと、うまく動かなかった。 ここは、クラス変数ではなく、クラスインスタンス変数を活用するのがポイント。 ちなみに、 Ruby1.9 だとクラス変数が子クラスで共有されなくなっているらしい。

これで新しいサービスもすぐに追加できるし、解析処理を Base にまとめたのでコードの見通しもよくなった。 どれくらいコードの見通しがよくなったかは、ソースの差分を見ると実感できる。 まぁ、もとのソースがひどいコピペだったんだけど。

もちろん、先に RSpec のコードを書いておいたので、安心してリファクタリングできた。 テストコードを書くことの安心感は、書いてみないと分からないなぁ…。

Gravatar サービスを追加

対応サービスに

Profile::Service::Gravatar

を追加した。 以下のように書くと、 Gravatar に登録したアイコンが表示される。 size オプションでサイズも指定できる。

<%= profile('mail@example.com', :gravatar) %>
<%= profile('mail@example.com', :gravatar, :size => 40) %>

キャッシュファイルのロジック変更

キャッシュファイルに VERSION 属性を持たせて、プラグインが更新された場合は古いキャッシュを使わないようにした。

ToDo

やるかもしれないし、やらないかもしれない。

  • スタイルシートをプラグイン内に書いているのをどうにかしたい。 → どこに書くのが tDiary の標準?
  • キャッシュ処理をクラスに切り出して、 RSpec でテストできるようにしたい。
  • profile.rb という名前が、 Ruby のライブラリと衝突していることに気がついた。どうしよう。

関連する日記