minor: some minor details

This commit is contained in:
Kyle Banker 2010-04-05 14:09:06 -04:00
parent c8fc728421
commit 8426a06244
14 changed files with 1483 additions and 0 deletions

46
lib/bson.rb Normal file
View File

@ -0,0 +1,46 @@
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
module BSON
VERSION = "0.20"
def self.serialize(obj, check_keys=false, move_id=false)
BSON_CODER.serialize(obj, check_keys, move_id)
end
def self.deserialize(buf=nil)
BSON_CODER.deserialize(buf)
end
end
begin
# Need this for running test with and without c ext in Ruby 1.9.
raise LoadError if ENV['TEST_MODE'] && !ENV['C_EXT']
require 'bson_ext/cbson'
raise LoadError unless defined?(CBson::VERSION)# && CBson::VERSION == Mongo::BSON::VERSION
require 'bson/bson_c'
module BSON
BSON_CODER = BSON_C
end
rescue LoadError
require 'bson/bson_ruby'
module BSON
BSON_CODER = BSON_RUBY
end
warn "\n**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance."
warn " You can install the extension as follows:\n gem install mongo_ext\n"
warn " If you continue to receive this message after installing, make sure that the"
warn " mongo_ext gem is in your load path and that the mongo_ext and mongo gems are of the same version.\n"
end
require 'bson/types/binary'
require 'bson/types/code'
require 'bson/types/dbref'
require 'bson/types/objectid'
require 'bson/types/regexp_of_holding'
require 'bson/types/min_max_keys'
require 'base64'
require 'bson/ordered_hash'
require 'bson/byte_buffer'
require 'bson/bson_ruby'
require 'bson/exceptions'

BIN
lib/bson/.exceptions.rb.swp Normal file

Binary file not shown.

20
lib/bson/bson_c.rb Normal file
View File

@ -0,0 +1,20 @@
# A thin wrapper for the CBson class
module BSON
class BSON_C
def self.serialize(obj, check_keys=false, move_id=false)
ByteBuffer.new(CBson.serialize(obj, check_keys, move_id))
end
def self.deserialize(buf=nil)
if buf.is_a? String
to_deserialize = ByteBuffer.new(buf) if buf
else
buf = ByteBuffer.new(buf.to_a) if buf
end
buf.rewind
CBson.deserialize(buf.to_s)
end
end
end

601
lib/bson/bson_ruby.rb Normal file
View File

@ -0,0 +1,601 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License, version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ++
module BSON
# A BSON seralizer/deserializer in pure Ruby.
class BSON_RUBY
# why was this necessary?
#include Mongo
MINKEY = -1
EOO = 0
NUMBER = 1
STRING = 2
OBJECT = 3
ARRAY = 4
BINARY = 5
UNDEFINED = 6
OID = 7
BOOLEAN = 8
DATE = 9
NULL = 10
REGEX = 11
REF = 12
CODE = 13
SYMBOL = 14
CODE_W_SCOPE = 15
NUMBER_INT = 16
TIMESTAMP = 17
NUMBER_LONG = 18
MAXKEY = 127
def initialize
@buf = ByteBuffer.new
end
if RUBY_VERSION >= '1.9'
def self.to_utf8(str)
str.encode("utf-8")
end
else
def self.to_utf8(str)
begin
str.unpack("U*")
rescue => ex
raise InvalidStringEncoding, "String not valid utf-8: #{str}"
end
str
end
end
def self.serialize_cstr(buf, val)
buf.put_array(to_utf8(val.to_s).unpack("C*") << 0)
end
def self.serialize_key(buf, key)
raise InvalidDocument, "Key names / regex patterns must not contain the NULL byte" if key.include? "\x00"
self.serialize_cstr(buf, key)
end
def to_a
@buf.to_a
end
def to_s
@buf.to_s
end
# Serializes an object.
# Implemented to ensure an API compatible with BSON extension.
def self.serialize(obj, check_keys=false, move_id=false)
new.serialize(obj, check_keys, move_id)
end
def self.deserialize(buf=nil)
new.deserialize(buf)
end
def serialize(obj, check_keys=false, move_id=false)
raise "Document is null" unless obj
@buf.rewind
# put in a placeholder for the total size
@buf.put_int(0)
# Write key/value pairs. Always write _id first if it exists.
if move_id
if obj.has_key? '_id'
serialize_key_value('_id', obj['_id'], false)
elsif obj.has_key? :_id
serialize_key_value('_id', obj[:_id], false)
end
obj.each {|k, v| serialize_key_value(k, v, check_keys) unless k == '_id' || k == :_id }
else
if obj.has_key?('_id') && obj.has_key?(:_id)
obj['_id'] = obj.delete(:_id)
end
obj.each {|k, v| serialize_key_value(k, v, check_keys) }
end
serialize_eoo_element(@buf)
if @buf.size > 4 * 1024 * 1024
raise InvalidDocument, "Document is too large (#{@buf.size}). BSON documents are limited to 4MB (#{4 * 1024 * 1024})."
end
@buf.put_int(@buf.size, 0)
self
end
# Returns the array stored in the buffer.
# Implemented to ensure an API compatible with BSON extension.
def unpack(arg)
@buf.to_a
end
def serialize_key_value(k, v, check_keys)
k = k.to_s
if check_keys
if k[0] == ?$
raise InvalidName.new("key #{k} must not start with '$'")
end
if k.include? ?.
raise InvalidName.new("key #{k} must not contain '.'")
end
end
type = bson_type(v)
case type
when STRING, SYMBOL
serialize_string_element(@buf, k, v, type)
when NUMBER, NUMBER_INT
serialize_number_element(@buf, k, v, type)
when OBJECT
serialize_object_element(@buf, k, v, check_keys)
when OID
serialize_oid_element(@buf, k, v)
when ARRAY
serialize_array_element(@buf, k, v, check_keys)
when REGEX
serialize_regex_element(@buf, k, v)
when BOOLEAN
serialize_boolean_element(@buf, k, v)
when DATE
serialize_date_element(@buf, k, v)
when NULL
serialize_null_element(@buf, k)
when REF
serialize_dbref_element(@buf, k, v)
when BINARY
serialize_binary_element(@buf, k, v)
when UNDEFINED
serialize_null_element(@buf, k)
when CODE_W_SCOPE
serialize_code_w_scope(@buf, k, v)
when MAXKEY
serialize_max_key_element(@buf, k)
when MINKEY
serialize_min_key_element(@buf, k)
else
raise "unhandled type #{type}"
end
end
def deserialize(buf=nil)
# If buf is nil, use @buf, assumed to contain already-serialized BSON.
# This is only true during testing.
if buf.is_a? String
@buf = ByteBuffer.new(buf) if buf
else
@buf = ByteBuffer.new(buf.to_a) if buf
end
@buf.rewind
@buf.get_int # eat message size
doc = OrderedHash.new
while @buf.more?
type = @buf.get
case type
when STRING, CODE
key = deserialize_cstr(@buf)
doc[key] = deserialize_string_data(@buf)
when SYMBOL
key = deserialize_cstr(@buf)
doc[key] = deserialize_string_data(@buf).intern
when NUMBER
key = deserialize_cstr(@buf)
doc[key] = deserialize_number_data(@buf)
when NUMBER_INT
key = deserialize_cstr(@buf)
doc[key] = deserialize_number_int_data(@buf)
when NUMBER_LONG
key = deserialize_cstr(@buf)
doc[key] = deserialize_number_long_data(@buf)
when OID
key = deserialize_cstr(@buf)
doc[key] = deserialize_oid_data(@buf)
when ARRAY
key = deserialize_cstr(@buf)
doc[key] = deserialize_array_data(@buf)
when REGEX
key = deserialize_cstr(@buf)
doc[key] = deserialize_regex_data(@buf)
when OBJECT
key = deserialize_cstr(@buf)
doc[key] = deserialize_object_data(@buf)
when BOOLEAN
key = deserialize_cstr(@buf)
doc[key] = deserialize_boolean_data(@buf)
when DATE
key = deserialize_cstr(@buf)
doc[key] = deserialize_date_data(@buf)
when NULL
key = deserialize_cstr(@buf)
doc[key] = nil
when UNDEFINED
key = deserialize_cstr(@buf)
doc[key] = nil
when REF
key = deserialize_cstr(@buf)
doc[key] = deserialize_dbref_data(@buf)
when BINARY
key = deserialize_cstr(@buf)
doc[key] = deserialize_binary_data(@buf)
when CODE_W_SCOPE
key = deserialize_cstr(@buf)
doc[key] = deserialize_code_w_scope_data(@buf)
when TIMESTAMP
key = deserialize_cstr(@buf)
doc[key] = [deserialize_number_int_data(@buf),
deserialize_number_int_data(@buf)]
when MAXKEY
key = deserialize_cstr(@buf)
doc[key] = MaxKey.new
when MINKEY, 255 # This is currently easier than unpack the type byte as an unsigned char.
key = deserialize_cstr(@buf)
doc[key] = MinKey.new
when EOO
break
else
raise "Unknown type #{type}, key = #{key}"
end
end
@buf.rewind
doc
end
# For debugging.
def hex_dump
str = ''
@buf.to_a.each_with_index { |b,i|
if (i % 8) == 0
str << "\n" if i > 0
str << '%4d: ' % i
else
str << ' '
end
str << '%02X' % b
}
str
end
def deserialize_date_data(buf)
unsigned = buf.get_long()
# see note for deserialize_number_long_data below
milliseconds = unsigned >= 2 ** 64 / 2 ? unsigned - 2**64 : unsigned
Time.at(milliseconds.to_f / 1000.0).utc # at() takes fractional seconds
end
def deserialize_boolean_data(buf)
buf.get == 1
end
def deserialize_number_data(buf)
buf.get_double
end
def deserialize_number_int_data(buf)
# sometimes ruby makes me angry... why would the same code pack as signed
# but unpack as unsigned
unsigned = buf.get_int
unsigned >= 2**32 / 2 ? unsigned - 2**32 : unsigned
end
def deserialize_number_long_data(buf)
# same note as above applies here...
unsigned = buf.get_long
unsigned >= 2 ** 64 / 2 ? unsigned - 2**64 : unsigned
end
def deserialize_object_data(buf)
size = buf.get_int
buf.position -= 4
object = BSON_CODER.new().deserialize(buf.get(size))
if object.has_key? "$ref"
DBRef.new(object["$ref"], object["$id"])
else
object
end
end
def deserialize_array_data(buf)
h = deserialize_object_data(buf)
a = []
h.each { |k, v| a[k.to_i] = v }
a
end
def deserialize_regex_data(buf)
str = deserialize_cstr(buf)
options_str = deserialize_cstr(buf)
options = 0
options |= Regexp::IGNORECASE if options_str.include?('i')
options |= Regexp::MULTILINE if options_str.include?('m')
options |= Regexp::EXTENDED if options_str.include?('x')
options_str.gsub!(/[imx]/, '') # Now remove the three we understand
if options_str == ''
Regexp.new(str, options)
else
warn("Using deprecated Regexp options #{options_str}; future versions of this MongoDB driver will support only i, m, and x. See deprecated class RegexpOfHolding for more info.")
RegexpOfHolding.new(str, options, options_str)
end
end
def deserialize_string_data(buf)
len = buf.get_int
bytes = buf.get(len)
str = bytes[0..-2]
if str.respond_to? "pack"
str = str.pack("C*")
end
if RUBY_VERSION >= '1.9'
str.force_encoding("utf-8")
end
str
end
def deserialize_code_w_scope_data(buf)
buf.get_int
len = buf.get_int
code = buf.get(len)[0..-2]
if code.respond_to? "pack"
code = code.pack("C*")
end
if RUBY_VERSION >= '1.9'
code.force_encoding("utf-8")
end
scope_size = buf.get_int
buf.position -= 4
scope = BSON_CODER.new().deserialize(buf.get(scope_size))
Code.new(code, scope)
end
def deserialize_oid_data(buf)
ObjectID.new(buf.get(12))
end
def deserialize_dbref_data(buf)
ns = deserialize_string_data(buf)
oid = deserialize_oid_data(buf)
DBRef.new(ns, oid)
end
def deserialize_binary_data(buf)
len = buf.get_int
type = buf.get
len = buf.get_int if type == Binary::SUBTYPE_BYTES
Binary.new(buf.get(len), type)
end
def serialize_eoo_element(buf)
buf.put(EOO)
end
def serialize_null_element(buf, key)
buf.put(NULL)
self.class.serialize_key(buf, key)
end
def serialize_dbref_element(buf, key, val)
oh = OrderedHash.new
oh['$ref'] = val.namespace
oh['$id'] = val.object_id
serialize_object_element(buf, key, oh, false)
end
def serialize_binary_element(buf, key, val)
buf.put(BINARY)
self.class.serialize_key(buf, key)
bytes = val.to_a
num_bytes = bytes.length
subtype = val.respond_to?(:subtype) ? val.subtype : Binary::SUBTYPE_BYTES
if subtype == Binary::SUBTYPE_BYTES
buf.put_int(num_bytes + 4)
buf.put(subtype)
buf.put_int(num_bytes)
buf.put_array(bytes)
else
buf.put_int(num_bytes)
buf.put(subtype)
buf.put_array(bytes)
end
end
def serialize_boolean_element(buf, key, val)
buf.put(BOOLEAN)
self.class.serialize_key(buf, key)
buf.put(val ? 1 : 0)
end
def serialize_date_element(buf, key, val)
buf.put(DATE)
self.class.serialize_key(buf, key)
millisecs = (val.to_f * 1000).to_i
buf.put_long(millisecs)
end
def serialize_number_element(buf, key, val, type)
if type == NUMBER
buf.put(type)
self.class.serialize_key(buf, key)
buf.put_double(val)
else
if val > 2**64 / 2 - 1 or val < -2**64 / 2
raise RangeError.new("MongoDB can only handle 8-byte ints")
end
if val > 2**32 / 2 - 1 or val < -2**32 / 2
buf.put(NUMBER_LONG)
self.class.serialize_key(buf, key)
buf.put_long(val)
else
buf.put(type)
self.class.serialize_key(buf, key)
buf.put_int(val)
end
end
end
def serialize_object_element(buf, key, val, check_keys, opcode=OBJECT)
buf.put(opcode)
self.class.serialize_key(buf, key)
buf.put_array(BSON_CODER.new.serialize(val, check_keys).to_a)
end
def serialize_array_element(buf, key, val, check_keys)
# Turn array into hash with integer indices as keys
h = OrderedHash.new
i = 0
val.each { |v| h[i] = v; i += 1 }
serialize_object_element(buf, key, h, check_keys, ARRAY)
end
def serialize_regex_element(buf, key, val)
buf.put(REGEX)
self.class.serialize_key(buf, key)
str = val.source
# We use serialize_key here since regex patterns aren't prefixed with
# length (can't contain the NULL byte).
self.class.serialize_key(buf, str)
options = val.options
options_str = ''
options_str << 'i' if ((options & Regexp::IGNORECASE) != 0)
options_str << 'm' if ((options & Regexp::MULTILINE) != 0)
options_str << 'x' if ((options & Regexp::EXTENDED) != 0)
options_str << val.extra_options_str if val.respond_to?(:extra_options_str)
# Must store option chars in alphabetical order
self.class.serialize_cstr(buf, options_str.split(//).sort.uniq.join)
end
def serialize_max_key_element(buf, key)
buf.put(MAXKEY)
self.class.serialize_key(buf, key)
end
def serialize_min_key_element(buf, key)
buf.put(MINKEY)
self.class.serialize_key(buf, key)
end
def serialize_oid_element(buf, key, val)
buf.put(OID)
self.class.serialize_key(buf, key)
buf.put_array(val.to_a)
end
def serialize_string_element(buf, key, val, type)
buf.put(type)
self.class.serialize_key(buf, key)
# Make a hole for the length
len_pos = buf.position
buf.put_int(0)
# Save the string
start_pos = buf.position
self.class.serialize_cstr(buf, val)
end_pos = buf.position
# Put the string size in front
buf.put_int(end_pos - start_pos, len_pos)
# Go back to where we were
buf.position = end_pos
end
def serialize_code_w_scope(buf, key, val)
buf.put(CODE_W_SCOPE)
self.class.serialize_key(buf, key)
# Make a hole for the length
len_pos = buf.position
buf.put_int(0)
buf.put_int(val.length + 1)
self.class.serialize_cstr(buf, val)
buf.put_array(BSON_CODER.new.serialize(val.scope).to_a)
end_pos = buf.position
buf.put_int(end_pos - len_pos, len_pos)
buf.position = end_pos
end
def deserialize_cstr(buf)
chars = ""
while true
b = buf.get
break if b == 0
chars << b.chr
end
if RUBY_VERSION >= '1.9'
chars.force_encoding("utf-8") # Mongo stores UTF-8
end
chars
end
def bson_type(o)
case o
when nil
NULL
when Integer
NUMBER_INT
when Float
NUMBER
when ByteBuffer
BINARY
when Code
CODE_W_SCOPE
when String
STRING
when Array
ARRAY
when Regexp
REGEX
when ObjectID
OID
when DBRef
REF
when true, false
BOOLEAN
when Time
DATE
when Hash
OBJECT
when Symbol
SYMBOL
when MaxKey
MAXKEY
when MinKey
MINKEY
when Numeric
raise InvalidDocument, "Cannot serialize the Numeric type #{o.class} as BSON; only Fixum, Bignum, and Float are supported."
when Date, DateTime
raise InvalidDocument, "#{o.class} is not currently supported; " +
"use a UTC Time instance instead."
else
if defined?(ActiveSupport::TimeWithZone) && o.is_a?(ActiveSupport::TimeWithZone)
raise InvalidDocument, "ActiveSupport::TimeWithZone is not currently supported; " +
"use a UTC Time instance instead."
else
raise InvalidDocument, "Cannot serialize #{o.class} as a BSON type; it either isn't supported or won't translate to BSON."
end
end
end
end
end

224
lib/bson/byte_buffer.rb Normal file
View File

@ -0,0 +1,224 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
# A byte buffer.
module BSON
class ByteBuffer
# Commonly-used integers.
INT_LOOKUP = {
0 => [0, 0, 0, 0],
1 => [1, 0, 0, 0],
2 => [2, 0, 0, 0],
3 => [3, 0, 0, 0],
4 => [4, 0, 0, 0],
2001 => [209, 7, 0, 0],
2002 => [210, 7, 0, 0],
2004 => [212, 7, 0, 0],
2005 => [213, 7, 0, 0],
2006 => [214, 7, 0, 0]
}
attr_reader :order
def initialize(initial_data=[])
@buf = initial_data
@cursor = @buf.length
@order = :little_endian
@int_pack_order = 'V'
@double_pack_order = 'E'
end
if RUBY_VERSION >= '1.9'
def self.to_utf8(str)
str.encode("utf-8")
end
else
def self.to_utf8(str)
begin
str.unpack("U*")
rescue => ex
raise InvalidStringEncoding, "String not valid utf-8: #{str}"
end
str
end
end
def self.serialize_cstr(buf, val)
buf.put_array(to_utf8(val.to_s).unpack("C*") + [0])
end
# +endianness+ should be :little_endian or :big_endian. Default is :little_endian
def order=(endianness)
@order = endianness
@int_pack_order = endianness == :little_endian ? 'V' : 'N'
@double_pack_order = endianness == :little_endian ? 'E' : 'G'
end
def rewind
@cursor = 0
end
def position
@cursor
end
def position=(val)
@cursor = val
end
def clear
@buf = []
rewind
end
def size
@buf.size
end
alias_method :length, :size
# Appends a second ByteBuffer object, +buffer+, to the current buffer.
def append!(buffer)
@buf = @buf + buffer.to_a
self
end
# Prepends a second ByteBuffer object, +buffer+, to the current buffer.
def prepend!(buffer)
@buf = buffer.to_a + @buf
self
end
def put(byte, offset=nil)
@cursor = offset if offset
@buf[@cursor] = byte
@cursor += 1
end
def put_array(array, offset=nil)
@cursor = offset if offset
@buf[@cursor, array.length] = array
@cursor += array.length
end
def put_int(i, offset=nil)
unless a = INT_LOOKUP[i]
a = []
[i].pack(@int_pack_order).each_byte { |b| a << b }
end
put_array(a, offset)
end
def put_long(i, offset=nil)
offset = @cursor unless offset
if @int_pack_order == 'N'
put_int(i >> 32, offset)
put_int(i & 0xffffffff, offset + 4)
else
put_int(i & 0xffffffff, offset)
put_int(i >> 32, offset + 4)
end
end
def put_double(d, offset=nil)
a = []
[d].pack(@double_pack_order).each_byte { |b| a << b }
put_array(a, offset)
end
# If +size+ == nil, returns one byte. Else returns array of bytes of length
# # +size+.
def get(len=nil)
one_byte = len.nil?
len ||= 1
check_read_length(len)
start = @cursor
@cursor += len
if one_byte
@buf[start]
else
if @buf.respond_to? "unpack"
@buf[start, len].unpack("C*")
else
@buf[start, len]
end
end
end
def get_int
check_read_length(4)
vals = ""
(@cursor..@cursor+3).each { |i| vals << @buf[i].chr }
@cursor += 4
vals.unpack(@int_pack_order)[0]
end
def get_long
i1 = get_int
i2 = get_int
if @int_pack_order == 'N'
(i1 << 32) + i2
else
(i2 << 32) + i1
end
end
def get_double
check_read_length(8)
vals = ""
(@cursor..@cursor+7).each { |i| vals << @buf[i].chr }
@cursor += 8
vals.unpack(@double_pack_order)[0]
end
def more?
@cursor < @buf.size
end
def to_a
if @buf.respond_to? "unpack"
@buf.unpack("C*")
else
@buf
end
end
def unpack(args)
to_a
end
def to_s
if @buf.respond_to? :fast_pack
@buf.fast_pack
elsif @buf.respond_to? "pack"
@buf.pack("C*")
else
@buf
end
end
def dump
@buf.each_with_index { |c, i| $stderr.puts "#{'%04d' % i}: #{'%02x' % c} #{'%03o' % c} #{'%s' % c.chr} #{'%3d' % c}" }
end
private
def check_read_length(len)
raise "attempt to read past end of buffer" if @cursor + len > @buf.length
end
end
end

39
lib/bson/exceptions.rb Normal file
View File

@ -0,0 +1,39 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
module BSON
# Generic Mongo Ruby Driver exception class.
class MongoRubyError < StandardError; end
# Raised when MongoDB itself has returned an error.
class MongoDBError < RuntimeError; end
# This will replace MongoDBError.
class BSONError < MongoDBError; end
# Raised when given a string is not valid utf-8 (Ruby 1.8 only).
class InvalidStringEncoding < BSONError; end
# Raised when attempting to initialize an invalid ObjectID.
class InvalidObjectID < BSONError; end
# Raised when trying to insert a document that exceeds the 4MB limit or
# when the document contains objects that can't be serialized as BSON.
class InvalidDocument < BSONError; end
# Raised when an invalid name is used.
class InvalidName < BSONError; end
end

140
lib/bson/ordered_hash.rb Normal file
View File

@ -0,0 +1,140 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
# A hash in which the order of keys are preserved.
#
# Under Ruby 1.9 and greater, this class has no added methods because Ruby's
# Hash already keeps its keys ordered by order of insertion.
class OrderedHash < Hash
def ==(other)
begin
!other.nil? &&
keys == other.keys &&
values == other.values
rescue
false
end
end
# We only need the body of this class if the RUBY_VERSION is before 1.9
if RUBY_VERSION < '1.9'
attr_accessor :ordered_keys
def self.[] *args
oh = OrderedHash.new
if Hash === args[0]
oh.merge! args[0]
elsif (args.size % 2) != 0
raise ArgumentError, "odd number of elements for Hash"
else
0.step(args.size - 1, 2) do |key|
value = key + 1
oh[args[key]] = args[value]
end
end
oh
end
def initialize(*a, &b)
super
@ordered_keys = []
end
def keys
@ordered_keys || []
end
def []=(key, value)
@ordered_keys ||= []
@ordered_keys << key unless @ordered_keys.include?(key)
super(key, value)
end
def each
@ordered_keys ||= []
@ordered_keys.each { |k| yield k, self[k] }
self
end
alias :each_pair :each
def to_a
@ordered_keys ||= []
@ordered_keys.map { |k| [k, self[k]] }
end
def values
collect { |k, v| v }
end
def merge(other)
oh = self.dup
oh.merge!(other)
oh
end
def merge!(other)
@ordered_keys ||= []
@ordered_keys += other.keys # unordered if not an OrderedHash
@ordered_keys.uniq!
super(other)
end
alias :update :merge!
def inspect
str = '{'
str << (@ordered_keys || []).collect { |k| "\"#{k}\"=>#{self.[](k).inspect}" }.join(", ")
str << '}'
end
def delete(key, &block)
@ordered_keys.delete(key) if @ordered_keys
super
end
def delete_if(&block)
self.each { |k,v|
if yield k, v
delete(k)
end
}
end
def clear
super
@ordered_keys = []
end
def hash
code = 17
each_pair do |key, value|
code = 37 * code + key.hash
code = 37 * code + value.hash
end
code & 0x7fffffff
end
def eql?(o)
if o.instance_of? OrderedHash
self.hash == o.hash
else
false
end
end
end
end

Binary file not shown.

54
lib/bson/types/binary.rb Normal file
View File

@ -0,0 +1,54 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
require 'bson/byte_buffer'
module BSON
# An array of binary bytes with a MongoDB subtype. See the subtype
# constants for reference.
#
# Use this class when storing binary data in documents.
class Binary < ByteBuffer
SUBTYPE_BYTES = 0x02
SUBTYPE_UUID = 0x03
SUBTYPE_MD5 = 0x05
SUBTYPE_USER_DEFINED = 0x80
# One of the SUBTYPE_* constants. Default is SUBTYPE_BYTES.
attr_accessor :subtype
# Create a buffer for storing binary data in MongoDB.
#
# @param [Array, String] data to story as BSON binary. If a string is given, the value will be
# concerted to an array of bytes using String#unpack("c*").
# @param [Fixnum] one of four values specifying a BSON binary subtype. Possible values are
# SUBTYPE_BYTES, SUBTYPE_UUID, SUBTYPE_MD5, and SUBTYPE_USER_DEFINED.
#
# @see http://www.mongodb.org/display/DOCS/BSON#BSON-noteondatabinary BSON binary subtypes.
def initialize(data=[], subtype=SUBTYPE_BYTES)
data = data.unpack("c*") if data.is_a?(String)
super(data)
@subtype = subtype
end
def inspect
"<BSON::Binary:#{object_id}>"
end
end
end

36
lib/bson/types/code.rb Normal file
View File

@ -0,0 +1,36 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
module BSON
# JavaScript code to be evaluated by MongoDB.
class Code < String
# Hash mapping identifiers to their values
attr_accessor :scope
# Wrap code to be evaluated by MongoDB.
#
# @param [String] code the JavaScript code.
# @param [Hash] a document mapping identifiers to values, which
# represent the scope in which the code is to be executed.
def initialize(code, scope={})
super(code)
@scope = scope
end
end
end

40
lib/bson/types/dbref.rb Normal file
View File

@ -0,0 +1,40 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
module BSON
# A reference to another object in a MongoDB database.
class DBRef
attr_reader :namespace, :object_id
# Create a DBRef. Use this class in conjunction with DB#dereference.
#
# @param [String] a collection name
# @param [ObjectID] an object id
#
# @core dbrefs constructor_details
def initialize(namespace, object_id)
@namespace = namespace
@object_id = object_id
end
def to_s
"ns: #{namespace}, id: #{object_id}"
end
end
end

View File

@ -0,0 +1,58 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
module BSON
# A class representing the BSON MaxKey type. MaxKey will always compare greater than
# all other BSON types and values.
#
# @example Sorting (assume @numbers is a collection):
#
# >> @numbers.save({"n" => Mongo::MaxKey.new})
# >> @numbers.save({"n" => 0})
# >> @numbers.save({"n" => 5_000_000})
# >> @numbers.find.sort("n").to_a
# => [{"_id"=>4b5a050c238d3bace2000004, "n"=>0},
# {"_id"=>4b5a04e6238d3bace2000002, "n"=>5_000_000},
# {"_id"=>4b5a04ea238d3bace2000003, "n"=>#<Mongo::MaxKey:0x1014ef410>},
# ]
class MaxKey
def ==(obj)
obj.class == MaxKey
end
end
# A class representing the BSON MinKey type. MinKey will always compare less than
# all other BSON types and values.
#
# @example Sorting (assume @numbers is a collection):
#
# >> @numbers.save({"n" => Mongo::MinKey.new})
# >> @numbers.save({"n" => -1_000_000})
# >> @numbers.save({"n" => 1_000_000})
# >> @numbers.find.sort("n").to_a
# => [{"_id"=>4b5a050c238d3bace2000004, "n"=>#<Mongo::MinKey:0x1014ef410>},
# {"_id"=>4b5a04e6238d3bace2000002, "n"=>-1_000_000},
# {"_id"=>4b5a04ea238d3bace2000003, "n"=>1_000_000},
# ]
class MinKey
def ==(obj)
obj.class == MinKey
end
end
end

180
lib/bson/types/objectid.rb Normal file
View File

@ -0,0 +1,180 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
require 'thread'
require 'socket'
require 'digest/md5'
module BSON
# Generates MongoDB object ids.
#
# @core objectids
class ObjectID
@@lock = Mutex.new
@@index = 0
# Create a new object id. If no parameter is given, an id corresponding
# to the ObjectID BSON data type will be created. This is a 12-byte value
# consisting of a 4-byte timestamp, a 3-byte machine id, a 2-byte process id,
# and a 3-byte counter.
#
# @param [Array] data should be an array of bytes. If you want
# to generate a standard MongoDB object id, leave this argument blank.
def initialize(data=nil)
@data = data || generate
end
# Determine if the supplied string is legal. Legal strings will
# consist of 24 hexadecimal characters.
#
# @param [String] str
#
# @return [Boolean]
def self.legal?(str)
len = 24
str =~ /([0-9a-f]+)/i
match = $1
str && str.length == len && match == str
end
# Create an object id from the given time. This is useful for doing range
# queries; it works because MongoDB's object ids begin
# with a timestamp.
#
# @param [Time] time a utc time to encode as an object id.
#
# @return [Mongo::ObjectID]
#
# @example Return all document created before Jan 1, 2010.
# time = Time.utc(2010, 1, 1)
# time_id = ObjectID.from_time(time)
# collection.find({'_id' => {'$lt' => time_id}})
def self.from_time(time)
self.new([time.to_i,0,0].pack("NNN").unpack("C12"))
end
# Adds a primary key to the given document if needed.
#
# @param [Hash] doc a document requiring an _id.
#
# @return [Mongo::ObjectID, Object] returns a newly-created or
# current _id for the given document.
def self.create_pk(doc)
doc.has_key?(:_id) || doc.has_key?('_id') ? doc : doc.merge!(:_id => self.new)
end
# Check equality of this object id with another.
#
# @param [Mongo::ObjectID] object_id
def eql?(object_id)
@data == object_id.instance_variable_get("@data")
end
alias_method :==, :eql?
# Get a unique hashcode for this object.
# This is required since we've defined an #eql? method.
#
# @return [Integer]
def hash
@data.hash
end
# Get an array representation of the object id.
#
# @return [Array]
def to_a
@data.dup
end
# Given a string representation of an ObjectID, return a new ObjectID
# with that value.
#
# @param [String] str
#
# @return [Mongo::ObjectID]
def self.from_string(str)
raise InvalidObjectID, "illegal ObjectID format" unless legal?(str)
data = []
12.times do |i|
data[i] = str[i * 2, 2].to_i(16)
end
self.new(data)
end
# Get a string representation of this object id.
#
# @return [String]
def to_s
str = ' ' * 24
12.times do |i|
str[i * 2, 2] = '%02x' % @data[i]
end
str
end
def inspect
"ObjectID('#{to_s}')"
end
# Convert to MongoDB extended JSON format. Since JSON includes type information,
# but lacks an ObjectID type, this JSON format encodes the type using an $id key.
#
# @return [String] the object id represented as MongoDB extended JSON.
def to_json(escaped=false)
"{\"$oid\": \"#{to_s}\"}"
end
# Return the UTC time at which this ObjectID was generated. This may
# be used in lieu of a created_at timestamp since this information
# is always encoded in the object id.
#
# @return [Time] the time at which this object was created.
def generation_time
Time.at(@data.pack("C4").unpack("N")[0]).utc
end
private
# We need to define this method only if CBson isn't loaded.
unless defined? CBson
def generate
oid = ''
# 4 bytes current time
time = Time.new.to_i
oid += [time].pack("N")
# 3 bytes machine
oid += Digest::MD5.digest(Socket.gethostname)[0, 3]
# 2 bytes pid
oid += [Process.pid % 0xFFFF].pack("n")
# 3 bytes inc
oid += [get_inc].pack("N")[1, 3]
oid.unpack("C12")
end
end
def get_inc
@@lock.synchronize do
@@index = (@@index + 1) % 0xFFFFFF
end
end
end
end

View File

@ -0,0 +1,45 @@
# --
# Copyright (C) 2008-2010 10gen Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
module BSON
# A Regexp that can hold on to extra options and ignore them. Mongo
# regexes may contain option characters beyond 'i', 'm', and 'x'. (Note
# that Mongo only uses those three, but that regexes coming from other
# languages may store different option characters.)
#
# Note that you do not have to use this class at all if you wish to
# store regular expressions in Mongo. The Mongo and Ruby regex option
# flags are the same. Storing regexes is discouraged, in any case.
#
# @deprecated
class RegexpOfHolding < Regexp
attr_accessor :extra_options_str
# @deprecated we're no longer supporting this.
# +str+ and +options+ are the same as Regexp. +extra_options_str+
# contains all the other flags that were in Mongo but we do not use or
# understand.
def initialize(str, options, extra_options_str)
warn "RegexpOfHolding is deprecated; the modifiers i, m, and x will be stored automatically as BSON." +
"If you're only storing the options i, m, and x, you can safely ignore this message."
super(str, options)
@extra_options_str = extra_options_str
end
end
end