module CouchbaseOrm module Index private def index(attrs, name = nil, presence: true, &processor) attrs = Array(attrs).flatten name ||= attrs.map(&:to_s).join('_') find_by_method = "find_by_#{name}" processor_method = "process_#{name}" bucket_key_method = "#{name}_bucket_key" bucket_key_vals_method = "#{name}_bucket_key_vals" class_bucket_key_method = "generate_#{bucket_key_method}" original_bucket_key_var = "@original_#{bucket_key_method}" #---------------- # keys #---------------- # class method to generate a bucket key given input values define_singleton_method(class_bucket_key_method) do |*values| processed = self.send(processor_method, *values) "#{@design_document}#{name}-#{processed}" end # instance method that uses the class method to generate a bucket key # given the current value of each of the key's component attributes define_method(bucket_key_method) do |args = nil| self.class.send(class_bucket_key_method, *self.send(bucket_key_vals_method)) end # collect a list of values for each key component attribute define_method(bucket_key_vals_method) do attrs.collect {|attr| self[attr]} end #---------------- # helpers #---------------- # simple wrapper around the processor proc if supplied define_singleton_method(processor_method) do |*values| if processor processor.call(values.length == 1 ? values.first : values) else values.join('-') end end # use the bucket key as an index - lookup records by attr values define_singleton_method(find_by_method) do |*values| key = self.send(class_bucket_key_method, *values) id = self.bucket.get(key, quiet: true) if id mod = self.find_by_id(id) return mod if mod # Clean up record if the id doesn't exist self.bucket.delete(key, quiet: true) end nil end #---------------- # validations #---------------- # ensure each component of the unique key is present if presence attrs.each do |attr| validates attr, presence: true define_attribute_methods attr end end define_method("#{name}_unique?") do values = self.send(bucket_key_vals_method) other = self.class.send(find_by_method, *values) !other || other.id == self.id end #---------------- # callbacks #---------------- # before a save is complete, while changes are still available, store # a copy of the current bucket key for comparison if any of the key # components have been modified before_save do |record| if attrs.any? { |attr| record.changes.include?(attr) } args = attrs.collect { |attr| send(:"#{attr}_was") || send(attr) } instance_variable_set(original_bucket_key_var, self.class.send(class_bucket_key_method, *args)) end end # after the values are persisted, delete the previous key and store the # new one. the id of the current record is used as the key's value. after_save do |record| original_key = instance_variable_get(original_bucket_key_var) if original_key check_ref_id = record.class.bucket.get(original_key, extended: true, quiet: true) if check_ref_id && check_ref_id.value == record.id begin record.class.bucket.delete(original_key, cas: check_ref_id.cas) rescue ::Libcouchbase::Error::KeyExists # Errors here can be ignored. Just means the key was updated elswhere end end end unless presence == false && attrs.length == 1 && record[attrs[0]].nil? record.class.bucket.set(record.send(bucket_key_method), record.id, plain: true) end instance_variable_set(original_bucket_key_var, nil) end # cleanup by removing the bucket key before the record is deleted # TODO: handle unpersisted, modified component values before_destroy do |record| check_ref_id = record.class.bucket.get(record.send(bucket_key_method), extended: true, quiet: true) if check_ref_id && check_ref_id.value == record.id begin record.class.bucket.delete(record.send(bucket_key_method), cas: check_ref_id.cas) rescue ::Libcouchbase::Error::KeyExists # Errors here can be ignored. Just means the key was updated elswhere end end true end # return the name used to construct the added method names so other # code can call the special index methods easily return name end end end