require 'date' # Only so we know what one is. Argh. module Candygram # Utility methods to serialize and unserialize objects into BSON module Wrapper BSON_SAFE = [String, NilClass, TrueClass, FalseClass, Fixnum, Float, Time, Regexp, ByteBuffer, Mongo::ObjectID, Mongo::Code, Mongo::DBRef] # Makes an object safe for the sharp pointy edges of MongoDB. Types properly serialized # by the BSON.serialize call get passed through unmolested; others are unpacked and their # pieces individually shrink-wrapped. def self.wrap(thing) # Pass the simple cases through return thing if BSON_SAFE.include?(thing.class) case thing when Symbol wrap_symbol(thing) when Array wrap_array(thing) when Hash wrap_hash(thing) when Numeric # The most obvious are in BSON_SAFE, but not all thing when Date thing.to_time else wrap_object(thing) # Our catchall machinery end end # Takes an array and returns the same array with unsafe objects wrapped def self.wrap_array(array) array.map {|element| wrap(element)} end # Takes a hash and returns it with both keys and values wrapped def self.wrap_hash(hash) wrapped = {} hash.each {|k, v| wrapped[wrap(k)] = wrap(v)} wrapped end # Returns a string that's distinctive enough for us to unwrap later and produce the same symbol def self.wrap_symbol(symbol) "__sym_" + symbol.to_s end # Returns a nested hash containing the class and instance variables of the object. It's not the # deepest we could ever go (it doesn't handle singleton methods, etc.) but it's a start. def self.wrap_object(object) wrapped = {"class" => object.class.name} ivars = {} object.instance_variables.each do |ivar| # Different Ruby versions spit different things out for instance_variables. Annoying. ivar_name = '@' + ivar.to_s.sub(/^@/,'') ivars[ivar_name] = wrap(object.instance_variable_get(ivar_name)) end wrapped["ivars"] = ivars unless ivars.empty? {"__object_" => wrapped} end # Undoes any complicated magic from the Wrapper.wrap method. Almost everything falls through # untouched except for symbol strings and hashed objects. def self.unwrap(thing) case thing when Hash if thing["__object_"] unwrap_object(thing) else unwrap_hash(thing) end when Array thing.map {|element| unwrap(element)} when /^__sym_(.+)/ $1.to_sym else thing end end # Traverses the hash, unwrapping both keys and values. Returns the hash that results. def self.unwrap_hash(hash) unwrapped = {} hash.each {|k,v| unwrapped[unwrap(k)] = unwrap(v)} unwrapped end # Turns a hashed object back into an object of the stated class, setting any captured instance # variables. The main limitation is that the object's class *must* respond to Class.new without # any parameters; we will not attempt to guess at any complex initialization behavior. def self.unwrap_object(hash) if innards = hash["__object_"] klass = Kernel.const_get(innards["class"]) object = klass.new if innards["ivars"] innards["ivars"].each do |name, value| object.instance_variable_set(name, unwrap(value)) end end object end end end end