require 'mongo_mapper' require_relative "mm_uses_uuid/version" require_relative "mm_uses_uuid/bson_binary_mixin" class BsonUuid def self.to_mongo(value) case value when String BSON::Binary.new(value, BSON::Binary::SUBTYPE_UUID) when BSON::Binary, NilClass value else raise "BsonUuid cannot be of type #{value.class}. String, BSON::Binary and NilClass are the only permitted types" end end def self.from_mongo(value) value end end class MongoMapper::Plugins::Associations::InArrayProxy def find_target return [] if ids.blank? if klass == UuidModel UuidModel.find(*criteria[:_id]) else all end end end class UuidModel include MongoMapper::Document @@lsn_class ||= [] def self.find(*args) args.flatten! ids_by_class = {} args.each do |id| lsn = id.to_s[-1].hex klass = @@lsn_class[lsn] raise "expected to find a class in @@lsn_class[#{lsn}] of the MongoMapper module but there was no entry. You need to set uuid_lsn in you class." if klass.nil? ids_by_class[klass] ||= [] ids_by_class[klass] << id end ids_by_class.map {|klass, ids| klass.find(ids)} .flatten end end module MmUsesUuid extend ActiveSupport::Concern included do key :_id, BsonUuid end module ClassMethods def find(*args) args.flatten! if args.size > 1 args.map! {|id| BsonUuid.to_mongo(id)} else args = BsonUuid.to_mongo(args.first) end super(args) end def new(params = {}) passed_id = params.delete(:id) || params.delete(:_id) || params.delete('id') || params.delete('_id') new_object = super(params) if passed_id.is_a?(BSON::Binary) and passed_id.subtype == BSON::Binary::SUBTYPE_UUID new_object.id = passed_id else new_object.find_new_uuid end new_object end def uuid_lsn(lsn_integer) add_class_lsn(self, lsn_integer) end def add_class_lsn(klass, lsn_integer) UuidModel.class_eval "@@lsn_class[#{lsn_integer}] = #{klass}" end end module InstanceMethods def find_new_uuid(options = {}) options = {force_safe: false}.merge(options) if not options[:ensure_unique_in] @_id, variant = make_uuid #puts "assuming #{variant} UUID #{@_id} is available" return else find_new_uuid_safely(options[:ensure_unique_in]) end end def find_new_uuid_safely(coll) @_id = nil begin trial_id, variant = make_uuid #puts "CHECKING #{coll} collection for availability of #{variant} UUID: #{trial_id}" if coll.where(:_id => trial_id).fields(:_id).first.nil? @_id = trial_id end end while @_id.nil? end def make_uuid uuid = SecureRandom.uuid.gsub!('-', '') lsn_class = UuidModel.class_variable_get('@@lsn_class') if replacement_lsn = lsn_class.index(self.class) uuid[-1] = replacement_lsn.to_s(16) end bson_encoded_uuid = BSON::Binary.new(uuid, BSON::Binary::SUBTYPE_UUID) return bson_encoded_uuid, 'random' end def id_to_s! @_id = @_id.to_s self end def id_to_s copy = self.clone copy.instance_variable_set '@_id', @_id.to_s copy end end end