BSONでFixnumはサポート外?

mongodbを使っていてこんなエラーが出た。

Cannot serialize Fixnum as a BSON type; it either isn't supported or won't translate to BSON

FixnumはBSONの型としてシリアライズできない?んなはずないじゃん。

BSON.serialize({"A" => 123})
#=> #<BSON::ByteBuffer:0x100661fd8 @cursor=4, @int_pack_order="V", @buf=[12, 0, 0, 0, 16, 65, 0, 123, 0, 0, 0, 0], @order=:little_endian, @double_pack_order="E">

だよね。

でもエラーが出るので、発生していたテストを見ると、Hashの値の方が

d = {"foo" => 123.minutes}

という感じになっていたので、確認してみる

require 'active_support'
#=> true

BSON.serialize({"A" => 123.minutes})
BSON::InvalidDocument: Cannot serialize Fixnum as a BSON type; it either isn't supported or won't translate to BSON.
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/../lib/bson/bson_ruby.rb:589:in `bson_type'
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/../lib/bson/bson_ruby.rb:138:in `serialize_key_value'
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/../lib/bson/bson_ruby.rb:111:in `serialize'
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/../lib/bson/bson_ruby.rb:111:in `each'
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/../lib/bson/bson_ruby.rb:111:in `serialize'
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/../lib/bson/bson_ruby.rb:85:in `serialize'
	from /Users/takeshi/.rvm/gems/ruby-1.8.7-p249@mm0.3/gems/bson-1.0/lib/bson.rb:6:in `serialize'
	from (irb):10

ついでに、こういうので変換された値のクラスはFixnumなんだけど、

d1 = 123.hours
#=> 442800 seconds
d1.class
#=> Fixnum
class << d1; self; end
#=> #<Class:#<ActiveSupport::Duration:0x10175e4d0>>

実際には、ActiveSupport::Durationが特異クラス。

で、ActiveSupport::Durationのソースコードによると、is_a?が、こんな感じになってるので、

    def is_a?(klass) #:nodoc:
      klass == Duration || super
    end

ActiveSupport::Durationをis_a?に渡すと判断できる

123.is_a?(ActiveSupport::Duration)
#=> false
123.hours.is_a?(ActiveSupport::Duration)
#=> true

だから、

d = {"foo" => 123.minutes}
d.each do |key, value|
   d[key] = value.to_i if value.is_a?(ActiveSupport::Duration)
end

としてあげれば、BSONでserialize可能なデータになります。

case に注意

caseに対応してなかったりするので、要注意。

def is_int?(val)
  case val
  when Integer then true
  else false
  end
end
#=> nil
is_int?(123)
#=> true
is_int?(123.hours)
#=> false