ruby

Rubyのクラスのインスタンス変数  [ruby]  [tips]

Rubyにはクラスにもインスタンス変数を作成することができる。

class Hoge
  def self.count_instance
    @num = 0 if @num.nil?   #クラスのインスタンス変数
    @num += 1
  end
  def self.get_count
    @num
  end
  def initialize(num)
    self.class.count_instance
    @num = num
    self
  end
  def get_count
    @num
  end
end

これがあるんならクラス変数(@@)とかっていらないのかな?

RubyのYAMLの変な挙動  [ruby]  [rails]  [bug_or_spec]

railsでto_yamlでactiverecordのオブジェクトをyaml化すると、日付関連がこのようになった。

mydate: !timestamp 08/10/10

で、これをrubyのYAML::loadで、読ませると'argument out of range'というエラーになる。

日付で問題なのはなぜか10月(他にもあるかもしれない。)


irb> require 'yaml'
irb> YAML::load("mydate: !timestamp 08/10/10")
ArgumentError: argument out of range
irb> YAML::load("mydate: !timestamp 08/11/10")
=> {"mydate"=>Thu Jan 10 00:00:00 UTC 2008}

(あれ?、よくみると11月も1月になってるし。。。)


irbでDateをyaml化すると、以下のようなフォーマットになる。

irb> Date.new(2008,10,10).to_yaml
=> "--- 2008-10-10\n"

なので、きっとrailsがto_yamlで変なことをしてるんだろう。


いろいろ調べて、environment.rbの記述を変更することで対処できた。

ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.update :default => "%Y/%m/%d"

なぜか、'%y'で設定してたので、'%Y'に変えた。


俺のミスってことだな。


Rubyで動的に関数定義  [ruby]  [tips]

module_eval(= class_eval),instance_evalがある。

なんで、この機能を調べているかというと、Railsで試験しているときに、ログインユーザを切り替えようと、一つのテスト関数内でログインを繰り返してたら、同じテスト関数内は同一セッションになって「すでにログインしてます」とかなったわけさ。(俺のインプリでは)


で、いちいちログアウトさせるのも面倒なので、いっそのことテスト関数を自動生成しようと思った。


そのうち詳しく書こうと思うので[todo]タグ。


追記:


module_eval(=class_eval)は、レシーバのクラスにインスタンス関数を定義。

irb> class Hoge
irb> end
irb> hoge = Hoge.new
irb> Hoge.module_eval {def fuga() p 'fuga';end}
irb> Hoge.fuga #=> error
irb> hoge.fuga #=> 'fuga'
irb> Hoge.new.fuga #=> 'fuga'

instance_evalは、レシーバがクラスの場合、クラス関数を定義(def self.xxxってやるやつと同等)

irb> class Hoge
irb> end
irb> hoge = Hoge.new
irb> Hoge.instance_eval {def fuga() p 'fuga';end}
irb> Hoge.fuga #=> 'fuga'
irb> hoge.fuga #=> error
irb> Hoge.new.fuga #=> error

instance_evalは、レシーバがobjectのインスタンス場合、そのobjectのインスタンス関数を定義

irb> class Hoge
irb> end
irb> hoge = Hoge.new
irb> hoge.instance_eval {def fuga() p 'fuga';end}
irb> Hoge.fuga #=> error
irb> hoge.fuga #=> 'fuga'
irb> Hoge.new.fuga #=> error

これに変数のスコープとからむとさらにややこしそうだ。

そのうち調べよう。ということでまだ[todo]

追記:

Rubyで動的に関数定義2 Rubyで動的に関数定義3(for Test::Unit)

Rubyで動的に関数定義2  [ruby]  [tips]

Rubyで動的に関数定義

からの続き。

生成後のインスタンスに関数を動的に生やす場合は、いろいろ試したが、以下の方法が一番よさそう。

インスタンス変数、クラスインスタンス変数、ローカル変数(クロージャ)が利用できる。


class Dog
  @dog_counter = 0
  def initialize(name)
    self.class.class_eval { @dog_counter += 1 }
    @dog_name = name
  end
  def perform
    puts "*****"
    puts "I am #{@dog_name}." # instance variable @
    puts "Total Dogs are #{self.class.class_eval {@dog_counter}}." # class instance variable @
    puts "bow!"
  end
end
dog_hoge = Dog.new('hoge')
dog_hoge.perform
dog_fuga = Dog.new('fuga')
dog_fuga.perform

インスタンスdog_fugaにだけ関数をオーバライドしてやる。

instance_eval, define_methodを利用することにより、外部ローカル変数を利用できる。


lexical_local_variable = 'Wooooo!'
(class << dog_fuga;self;end).instance_eval do
  define_method(:perform) do
    puts "*****"
    puts "I am #{@dog_name}." # instance variable @
    puts "I am one of the #{self.class.class_eval {@dog_counter}} Dogs." # class instance variable @
    puts "#{lexical_local_variable}" # lexical closure(local variable)
  end
end
dog_hoge.perform
dog_fuga.perform

いろいろ使えそうだ。


追記

Rubyで動的に関数定義3(for Test::Unit)

Rubyで動的に関数定義3(for Test::Unit)  [ruby]  [tips]  [TDD]

Rubyで動的に関数定義 Rubyで動的に関数定義2

からの続き。

で、今回いろいろ調べていた大元の理由である「テストケースの自動生成」だけど、こんな感じで使えそう。


テスト対象

class Testee
  def login(login_user)
    @login_user ||=login_user
  end
  def who_am_i?
    @login_user
  end
end

一つのテストケース内でログインの試験を複数回やると、初回のログイン情報が残るためにfailする。

require 'test/unit'
require 'testee' 

class TesteeTest < Test::Unit::TestCase
  def test_should_login
    @testee.login(:hoge)
    assert_equal :hoge, @testee.who_am_i? # => ok
    @testee.login(:fuga)
    assert_equal :fuga, @testee.who_am_i? #=> fail :hogeが残ってる
  end
end

今までなら、複数の試験を手書きしてた。


そこで、今回勉強してきた、define_methodを利用してテストケースを自動生成してみる。

require 'test/unit'
require 'testee' 

class TesteeTest < Test::Unit::TestCase
  def self.generate_test
    while login_name = DATA.gets do
      login_name.chomp!
      define_method("test_should_login_by_#{login_name}") do
        @testee.login(login_name)
        assert_equal login_name, @testee.who_am_i? , " error on login by #{login_name}"
      end
    end 
  end
  def setup
    @testee = Testee.new
  end
end
__END__
:hoge
:fuga

うまくやれば、PerlのTest::Baseくらい使い勝手が良くなるかもね。


Railsテスト時に日付を固定にする方法 (for Test::Unit)  [ruby]  [rails]  [TDD]

Railsで試験をする時に、日付に依存する機能(次の誕生日までの日数を求めるとか。。)がある時、今までならfixtureに

date: <%= Date.today().to_s %>

とか、動的にfixtureを定義していたのですが、いろいろロジックが複雑になりだしたので、発想を逆にすることにしました。

つまり、テストの日付を固定するという方法です。

基本的に、Date.today()や、Time.now()を上書きしてやるのですが、他のテストに影響を与えないようにやる必要があります。

そこで、Test::Unit::TestCaseのsetup/teardown内で、関数の再定義、もとの定義への戻しをやってやります。

hoge_controller_test.rb

:
#このテストは2008年9月で試験
class Date
  def self.fixed_today
    return Date.new(2008,9,1)
  end
end

class HogeControllerTest < Test::Unit::TestCase 
  def setup
    Date.instance_eval do
      alias :orig_today :today
      alias :today :fixed_today
    end
  end
  def teardown
    Date.instance_eval do
      alias :fixed_today :today
      alias :today :orig_today
    end
  end
:
end

特異メソッドのaliasを作成するので、instance_evalを利用してaliasによる関数の入れ替えをしています。

RubyでHash Slice  [ruby]

Rubyってハッシュスライスがないんだよね。

Perlで簡単に

@hash{@fields}=@values;

とかできることができない。。

かなりつらいんですけど。。

hashの一部をupdateする場合のやり方はこんな感じらしい。

hash.update(Hash[*keys.zip(vals).flatten])

ActionMailerでメール送信@Sakuraレンタルサーバ  [ruby]  [tips]  [sample_code]

Railsを使わないで、rubyからActionMailerでメールを送信したくなったので、やり方のメモ。

ActionMailerは、gemでインストール済みと仮定。

Sakuraレンタルサーバ(さくら)の場合、"POP before SMTP"なので、送信前にpop認証が必要となります。

require 'rubygems'
require 'action_mailer'
require 'net/pop'

class MyMailer < ActionMailer::Base
  alias_method :base_perform_delivery_smtp, :perform_delivery_smtp
  @@pop3_auth_done = nil
  def self.setup_for_sakura
    self.delivery_method = :smtp
    self.smtp_settings = {
      :address => 'xxx.sakura.ne.jp',
      :port => 587,
      :domain => 'xxx.sakura.ne.jp',
      :pop3_auth => {
        :server => 'xxx.sakura.ne.jp',
        :user_name => 'hoge@xxx.sakura.ne.jp',
        :password => 'passw0rd',
        :expires => 1.hour,
        :authentication => :login
      }
    } 
    self.template_root = File.dirname(__FILE__) + "/templates"
    self.perform_deliveries = true
  end
  def mail_contents(user,contents)
    from "hoge <hoge@xxx.sakura.ne.jp>"
    recipients "#{user} <#{user}>"
    subject "日本語タイトルほげふが"
    body :user => user, :contents => contents
  end
  private
  def perform_delivery_smtp(mail)
    do_pop_auth if !@@pop3_auth_done or (Time.now - smtp_settings[:pop3_auth][:expires]) >= @@pop3_auth_done
    base_perform_delivery_smtp(mail)
  end
  def do_pop_auth
    pop = Net::POP3.new(smtp_settings[:pop3_auth][:server])
    pop.start(smtp_settings[:pop3_auth][:user_name], smtp_settings[:pop3_auth][:password])
    @@pop3_auth_done = Time.now
    pop.finish
  end
end 

で、./templates/my_mailer/mail_contents.erbに、テンプレートを適当に作成。

<%= @user %> 様
<%= @contents[:hello] %>ですねん。
ほな

これを利用する側はこんな感じ。

require 'rubygems'
require File.dirname(__FILE__) + "/my_mailer"
MyMailer.setup_for_sakura
MyMailer.deliver_mail_report('user@hogefuga.com', {:hello => "こんにちは"})

rubygemsへのパスが通ってない場合は、$LOAD_PATH.push("パス"), ENV['GEM_HOME'] = "パス"、でライブラリ及びGEM_HOMEのパスを通しておきましょう。

参考: http://railsforum.com/viewtopic.php?pid=47672

メールアドレスのバリデーション by Ruby  [ruby]  [tips]  [sample_code]

javascriptによる簡易regexでは対応できないケースがでてしまったので、いろいろ参考にしつつruby版を作った。

require 'resolv'
require 'pp'

class MailAddressValidator
  def self.validate(address)
    return validate_by_regex(address) && validate_by_MX(address)
  end
  def self.validate_by_regex(address)
    addr_spec = %r{^(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|[^\\"])*")))\@(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\])))$}
    address =~addr_spec
  end
  def self.validate_by_MX(address)
      mxdomain = address[/[^@]+$/]
      Resolv::DNS.new.getresource(mxdomain,Resolv::DNS::Resource::IN::MX) rescue nil
  end
  private_class_method :validate_by_regex, :validate_by_MX
end

使うときはこんな感じ

require File.dirname(__FILE__) + '/mail_address_validator'
print (MailAddressValidator.validate('hogefuga@hogefugadennnenn.naiyo')) 'ok':'ng' #=> ng

これをつかって、同期Ajaxでチェックしてる。

日本語が変だな。。。要は javascriptでxmlhttprequest.open('GET',"xxxxxx", false)という形で同期的にチェックしてる。

参考:

http://blog.livedoor.jp/dankogai/archives/51189905.html

http://www.ruby-lang.org/ja/man/html/resolv.html

追記 2009-04-13:

spellミスを直しました。id:Uchimataさんありがとうございます。

はてなブックマークがはじめてコメント欄として機能した記念すべき日です。

Ruby メソッド呼び出しからオプション(Hash)部分を取り出す  [ruby]  [idiom]

railsで使えるidiomを見つけた。

rubyで、あるメソッドの呼び出しが以下のようになっている場合、

hoge(:param, :option1 => true , :option2 => false)

以下のやり方でオプション部分だけを取り出せる

def hoge(*prams)
  options = params.last.is_a?(::Hash)? params.pop : {}
  pp options
  pp params
end

参考:

ActiveSupport::CoreExtensions::Array::ExtractOptions#extract_options!

RubyでTest::Baseっぽいのを作る  [ruby]  [TDD]  [sample_code]

Rubyで動的に関数定義3

で書いた方法をつかって、PerlのTest::Baseっぽいのを書いてみる。

(Ruby Test::Baseってのがすでにあるみたいだけどね。)

まずは、test_base.rb

require 'test/unit' 
class TestBase < Test::Unit::TestCase
  @@test_template = {}
  def run(*args)
    return if@method_name.to_s == "default_test"
    super
  end
  def self.start_test
    self.parse if(@@test_template.empty?)
    self.generate_tests 
  end
  private
  def self.parse
    test_name = ""
    buf = ""
    buf_mode = ""
    while line = DATA.gets do 
      line.chomp!
      if(line =~ /^(===|---)/)
        if(buf != "" && test_name != "" && buf_mode != "")
          @@test_template[test_name][buf_mode] = eval(buf)
          buf = ""
        end
        if(line =~ /^=== (.*)$/)
          test_name = $1
          @@test_template[test_name] = {}
        elsif(line =~ /--- input (.*)$/)
          @@test_template[test_name][:method_name] = $1
          buf_mode = :input
        elsif(line = ~/--- expected/)
          buf_mode= :expected
        end
      else
        buf += line
      end
    end
    if(buf != "" && test_name != "" && buf_mode != "")
      @@test_template[test_name][buf_mode] = eval(buf)
      buf = ""
    end
  end
  def self.generate_tests
    @@test_template.each do |test_name,test_args|
      define_method(test_name) do
        result=__send__(test_args[:method_name],test_args[:input])
        assert_equal test_args[:expected], result, " error on #{test_name}"
      end
    end 
  end
end

DATAの処理のところが、ちょっと(かなり?)スパゲッティーだけど、generate_testsの所でテスト用の関数をDATA内容に合わせて作成してる。

Test::Unit::TestCaseのrunメソッドで"default_test"を無視するようにして、TestCaseがテスト未定義時に勝手にdefault_testを実行するのを防いでる。(この部分を抜くとどうなるか試せば言ってる意味がわかるはず)

実際のテストのほうは、こんな感じで書く。

require './test_base'
require './testee' 

class TestBaseTest < TestBase
  def setup #setupとかは通常のTestCaseと同じ
    @testee = Testee.new
  end
  def login_test(login_name) # DATAを利用するテストはtest_以外のメソッド名
    @testee.login(login_name)
    @testee.who_am_i?
  end
  def test_without_data # DATAを使わないテストはtest_で始まるメソッド名
    true
  end
end
TestBaseTest.start_test #最後にstart_testでテストを生成しておく
__END__ #ここからテストに渡すパラメータ
=== test_should_login_by_hoge
--- input login_test #inputの後ろに利用するメソッド名
:hoge
--- expected
:hoge
=== test_should_login_by_fuga
--- input login_test
:fuga
--- expected
:fuga

inputの後ろのメソッド名が一個しかかけなかったり、フィルタとか便利なものがなかったり、"ruby Test::Base"よりかなりはしょってるけど、1ファイルで出来てるのでその辺はしょうがないと割り切る。

じゃあの。

Rubyで動的にクラスファイル(プラグイン)を読み込む  [ruby]  [sample_code]

いろいろな処理をするファイルをプラグインとして適当なディレクトリに入れておくと、それらをロードしてオブジェクト化するやりかた。

バッチ処理とかでカスタムな方法をインプリしたいときとかに使えるかと思う。

plugin_loader.rb

class PluginLoader
  attr_reader :plugins
  def initialize(plugin_base, extension = 'rb')
    @plugin_base = plugin_base
    @plugins = []
    Dir.glob(@plugin_base + "*.#{extension}").each do |p|
      require p
      @plugins << Object.const_get(to_classname(p)).new
    end
  end
  private
  def to_classname(str)
    str.sub(/^#{@plugin_base}/){""}.sub(/\.rb$/){""}.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
  end
end

使い方はこんな感じ(あらかじめpluginsディレクトリにxxxx.rbファイルを作っておく)

test.rb

require './plugin_loader' 

pl = PluginLoader.new(File.dirname(__FILE__) + "/plugins/")
pl.plugins.each do | p |
  p.run
end

Ruby RSS::MakerでCDATAを扱えるようにしてみる  [ruby]  [sample_code]  [tips]

RubyのRSS::MakerでRSS1.0生成すると、content:encodedが自動的にhtml_escapeされてしまい、CDATAとして埋め込んでくれないので、モンキーパッチングしてみた。

rss_cdata.rb

require 'rss'

module RSS
  module BaseModel
    def install_cdata_element(tag_name, uri, occurs, name=nil, type=nil, disp_name=nil)
      name ||= tag_name
      disp_name ||= name
      self::ELEMENTS << name
      add_need_initialize_variable(name)
      install_model(tag_name, uri, occurs, name)

      def_corresponded_attr_writer name, type, disp_name
      convert_attr_reader name
      install_element(name) do |n, elem_name|
        <<-EOC
        if @#{n}
          rv = "\#{indent}<#{elem_name}>"
          value = "<![CDATA[" + eval("@#{n}") + "]]>"
          if need_convert
            rv << convert(value)
          else
            rv << value
          end
          rv << "</#{elem_name}>"
          rv
        else
          ''
        end
EOC
      end
    end
  end 
  
  module ContentModel
    def self.append_features(klass)
      super 
      
      klass.install_must_call_validator(CONTENT_PREFIX, CONTENT_URI)
      %w(encoded).each do |name|
        klass.install_cdata_element(name, CONTENT_URI, "?", "#{CONTENT_PREFIX}_#{name}")
      end
    end
  end 
  
  class RDF
    class Item; include ContentModel; end
  end
end

試してみる

require 'rss'
require './rss_cdata'
 
rss = RSS::Maker.make("1.0") do | maker |
  maker.channel.about = "http://hoge.fuga/index.rdf"
  maker.channel.title = "harahoro"
  maker.channel.description = "hirehare"
  maker.channel.link = "http://hoge.fuga/"
  item = maker.items.new_item
  item.link = "http://hoge.fuga/piyo.html"
  item.title = "aheaheuhiha"
  item.content_encoded = "<p>aheuhihaaa</p>"
  item.dc_date = Time.now()
end 
puts rss.to_s

結果

<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns:image="http://web.resource.org/rss/1.0/modules/image/"
  xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/"
  xmlns="http://purl.org/rss/1.0/">
  <channel rdf:about="http://hoge.fuga/index.rdf">
    <title>harahoro</title>
    <link>http://hoge.fuga/</link>
    <description>hirehare</description>
    <items>
      <rdf:Seq>
        <rdf:li resource="http://hoge.fuga/piyo.html"/>
      </rdf:Seq>
    </items>
    <taxo:topics>
      <rdf:Bag/>
    </taxo:topics>
  </channel>
  <item rdf:about="http://hoge.fuga/piyo.html">
    <title>aheaheuhiha</title>
    <link>http://hoge.fuga/piyo.html</link>
    <content:encoded><![CDATA[<p>aheuhihaaa</p>]]></content:encoded>
    <dc:date>2009-12-02T13:14:24+09:00</dc:date>
    <taxo:topics>
      <rdf:Bag/>
    </taxo:topics>
    <content:encoded><![CDATA[<p>aheuhihaaa</p>]]></content:encoded>
  </item>
</rdf:RDF>

どうよ。