Browse files

move Base methods into Mongomatic module, begin using rspec, some oth…

…er changes (see entire commit for details)

Beginning processes of making Mongomatic an includable module instead of a Base to subclass. Moved all functions (except #transaction, becasue it doesn't belong in main module) out of Mongomatic::Base and into Mongomatic module (plan is to move things out of this module into some sub-modules to clean it up, later). The move maintains backward compatibility so far by including the Mongomatic module in Base. The only thing that seems to be broken (when running the old tests) are the typed field checks, but I removed those explicitly so its expected. Note: I'm not sure how we want to handle backwards compat, if at all with the old _typed_field_ DSL (being replaced by _attribute_ DSL). 

Also, began writing specs to replace existing unit tests. Leaving the old tests around for now so we can try to maintain some semblance of backwards compat. 

Some other changes along the way: 

* fixed bugs in #has_key? when accessing an array of embedded documents by index. it now should work as expected for all dotted keys
* fixed bugs in #value_for_key and #set_value_for_key when dealing with arrays of embedded documents (small improvements on Tony Schneider's fix
* #[], #[]= now support use of mongodb dot notation when accessing keys. They are aliases for #value_for_key and #set_value_for_key respectively
* .count now uses collection.count. This is a minor improvement but reduces overhead of creating a useless Mongomatic::Cursor
* reload now uses find_one instead of find
* broke the to be deprecated/removed(?) typed_field syntax
  • Loading branch information...
1 parent 76ea5e8 commit e2cd0f70c54ab39c7fcca0f35d0658112640ca75 @jrwest jrwest committed May 27, 2011
Showing with 815 additions and 346 deletions.
  1. +1 −0 .autotest
  2. +1 −0 .rspec
  3. +7 −14 Gemfile.lock
  4. +343 −3 lib/mongomatic.rb
  5. +1 −329 lib/mongomatic/base.rb
  6. +403 −0 spec/mongomatic/mongomatic_spec.rb
  7. +59 −0 spec/spec_helper.rb
View
1 .autotest
@@ -0,0 +1 @@
+require 'autotest/bundler'
View
1 .rspec
@@ -0,0 +1 @@
+--color
View
21 Gemfile.lock
@@ -3,7 +3,6 @@ GEM
specs:
ZenTest (4.5.0)
activesupport (3.0.6)
- archive-tar-minitar (0.5.2)
autotest (4.4.6)
ZenTest (>= 4.4.1)
bson (1.3.1)
@@ -16,8 +15,7 @@ GEM
bundler (~> 1.0.0)
git (>= 1.2.5)
rake
- linecache19 (0.5.12)
- ruby_core_source (>= 0.1.4)
+ linecache (0.43)
minitest (2.0.2)
mongo (1.3.1)
bson (>= 1.3.1)
@@ -31,16 +29,11 @@ GEM
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
- ruby-debug-base19 (0.11.25)
- columnize (>= 0.3.1)
- linecache19 (>= 0.5.11)
- ruby_core_source (>= 0.1.4)
- ruby-debug19 (0.11.6)
- columnize (>= 0.3.1)
- linecache19 (>= 0.5.11)
- ruby-debug-base19 (>= 0.11.19)
- ruby_core_source (0.1.5)
- archive-tar-minitar (>= 0.5.2)
+ ruby-debug (0.10.4)
+ columnize (>= 0.1)
+ ruby-debug-base (~> 0.10.4.0)
+ ruby-debug-base (0.10.4)
+ linecache (>= 0.3)
yard (0.6.6)
PLATFORMS
@@ -58,5 +51,5 @@ DEPENDENCIES
mongo (>= 1.3.1)
rcov
rspec (~> 2.6.0)
- ruby-debug19
+ ruby-debug
yard (~> 0.6.5)
View
346 lib/mongomatic.rb
@@ -4,7 +4,13 @@
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash'
+require "#{File.dirname(__FILE__)}/mongomatic/m_hash"
+require "#{File.dirname(__FILE__)}/mongomatic/errors"
+require "#{File.dirname(__FILE__)}/mongomatic/cursor"
+
module Mongomatic
+
+ # Mongomatic Module Functions
class << self
# Returns an instance of Mongo::DB
def db
@@ -19,16 +25,350 @@ def db=(obj)
end; @db = obj
end
end
+
+ def self.included(klass)
+ klass.send(:attr_accessor, :removed, :is_new, :errors)
+ klass.send(:attr_reader, :doc)
+ klass.send(:extend, ClassMethods)
+ end
+
+ # Public Instance Methods
+ def initialize(doc_hash=Mongomatic::MHash.new, is_new=true)
+ self.doc = doc_hash
+ self.removed = false
+ self.is_new = is_new
+ self.errors = Mongomatic::Errors.new
+ do_callback(:after_initialize)
+ end
+
+ # Insert the document into the database. Will return false if the document has
+ # already been inserted or is invalid. Returns the generated BSON::ObjectId
+ # for the new document. Will silently fail if MongoDB is unable to insert the
+ # document, use insert! or send in {:safe => true} if you want a Mongo::OperationError.
+ # If you want to raise the following errors also, pass in {:raise => true}
+ # * Raises Mongomatic::Exceptions::DocumentNotNew if document is not new
+ # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
+ def insert(opts={})
+ if opts[:raise] == true
+ raise Mongomatic::Exceptions::DocumentWasRemoved if removed?
+ raise Mongomatic::Exceptions::DocumentNotNew unless new?
+ raise Mongomatic::Exceptions::DocumentNotValid unless valid?
+ else
+ return false unless new? && valid?
+ end
+
+ do_callback(:before_insert)
+ do_callback(:before_insert_or_update)
+ if ret = self.class.collection.insert(@doc,opts)
+ @doc["_id"] = @doc.delete(:_id) if @doc[:_id]
+ self.is_new = false
+ end
+ do_callback(:after_insert)
+ do_callback(:after_insert_or_update)
+ ret
+ end
+
+ # Calls insert(...) with {:safe => true} passed in as an option.
+ # * Raises Mongo::OperationFailure if there was a DB error on inserting
+ # If you want to raise the following errors also, pass in {:raise => true}
+ # * Raises Mongomatic::Exceptions::DocumentNotNew if document is not new
+ # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
+ def insert!(opts={})
+ insert(opts.merge(:safe => true))
+ end
+
+ # Will persist any changes you have made to the document. Silently fails on
+ # db update error. Use update! or pass in {:safe => true} to raise a
+ # Mongo::OperationError if that's what you want.
+ # If you want to raise the following errors also, pass in {:raise => true}
+ # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
+ # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
+ # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been removed
+ def update(opts={},update_doc=@doc)
+ if opts[:raise] == true
+ raise Mongomatic::Exceptions::DocumentWasRemoved if removed?
+ raise Mongomatic::Exceptions::DocumentIsNew if new?
+ raise Mongomatic::Exceptions::DocumentNotValid unless valid?
+ else
+ return false if new? || removed? || !valid?
+ end
+ do_callback(:before_update)
+ do_callback(:before_insert_or_update)
+ ret = self.class.collection.update({"_id" => @doc["_id"]}, update_doc, opts)
+ do_callback(:after_update)
+ do_callback(:after_insert_or_update)
+ ret
+ end
+
+ # Calls update(...) with {:safe => true} passed in as an option.
+ # * Raises Mongo::OperationError if there was a DB error on updating
+ # If you want to raise the following errors also, pass in {:raise => true}
+ # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
+ # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
+ # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been removed
+ def update!(opts={},update_doc=@doc)
+ update(opts.merge(:safe => true),update_doc)
+ end
+
+ # If the document is new then an insert is performed, otherwise, an update is peformed.
+ def save(opts={})
+ (new?) ? insert(opts) : update(opts)
+ end
+
+ # Calls save(...) with {:safe => true} passed in as an option.
+ def save!(opts={})
+ save(opts.merge(:safe => true))
+ end
+
+ # Remove this document from the collection. Silently fails on db error,
+ # use remove! or pass in {:safe => true} if you want an exception raised.
+ # If you want to raise the following errors also, pass in {:raise => true}
+ # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
+ # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been already removed
+ def remove(opts={})
+ if opts[:raise] == true
+ raise Mongomatic::Exceptions::DocumentWasRemoved if removed?
+ raise Mongomatic::Exceptions::DocumentIsNew if new?
+ else
+ return false if new? || removed?
+ end
+ do_callback(:before_remove)
+ if ret = self.class.collection.remove({"_id" => @doc["_id"]})
+ self.removed = true; freeze; ret
+ end
+ do_callback(:after_remove)
+ ret
+ end
+
+ # Calls remove(...) with {:safe => true} passed in as an option.
+ # * Raises Mongo::OperationError if there was a DB error on removing
+ # If you want to raise the following errors also, pass in {:raise => true}
+ # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
+ # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been already removed
+ def remove!(opts={})
+ remove(opts.merge(:safe => true))
+ end
+
+ # Reload the document from the database
+ def reload
+ if obj = self.class.find_one(@doc["_id"])
+ self.doc = obj.doc; true
+ end
+ end
+
+ # Check equality with another Mongomatic document
+ def ==(obj)
+ obj.is_a?(self.class) && obj.doc["_id"] == @doc["_id"]
+ end
+
+ # Returns true if document contains key
+ def has_key?(key)
+ field, res, depth = hash_for_field(key.to_s, true)
+ case res
+ when Hash
+ res.has_key?(field)
+ when Array
+ !res[key.split('.')[depth].to_i].nil?
+ end
+ end
+
+ def value_for_key(key)
+ field, res, depth = hash_for_field(key.to_s, true)
+ field_accessor = res.kind_of?(Hash) ? field : field.to_i
+ res[field_accessor]
+ end
+ alias :[] :value_for_key
+
+ def set_value_for_key(key, value)
+ field, res, depth = hash_for_field(key.to_s)
+ field_accessor = res.kind_of?(Hash) ? field : field.to_i
+ val = value.kind_of?(Hash) ? Mongomatic::MHash.new(value) : value
+ res[field_accessor] = val
+ end
+ alias :[]= :set_value_for_key
+
+ # Merge this document with the supplied hash. Useful for updates:
+ # mydoc.merge(params[:user])
+ def merge(hash)
+ hash.each { |k,v| self[k] = v }; @doc
+ end
+
+ ##
+ # Same as Hash#delete
+ #
+ # mydoc.delete("name")
+ # => "Ben"
+ # mydoc.has_hey?("name")
+ # => false
+ def delete(key)
+ @doc.delete(key)
+ end
+
+
+ # Return this document as a hash.
+ def to_hash
+ @doc || {}
+ end
+
+ def valid?
+# check_typed_fields! REMOVING TYPE FIELDS CHECK FOR NOW
+ self.errors = Mongomatic::Errors.new
+ do_callback(:before_validate)
+ validate
+ do_callback(:after_validate)
+ self.errors.empty?
+ end
+
+ def doc=(hash)
+ hash = Mongomatic::MHash.new(hash) unless hash.is_a?(Mongomatic::MHash)
+ @doc = hash
+ end
+
+ def new?
+ self.is_new == true
+ end
+
+ def is_new?
+ !!new?
+ end
+
+ # Will return true if the document has been removed.
+ def removed?
+ self.removed == true
+ end
+
+ # Private Instance Methods
+ def do_callback(meth)
+ notify(meth) if self.class.included_modules.include?(Mongomatic::Observable) # TODO entire block is smelly, doesnt belong here
+ return false unless respond_to?(meth, true)
+ send(meth)
+ end
+ private :do_callback
+
+ def hash_for_field(field, break_if_dne=false)
+ parts = field.split(".")
+ curr_hash = self.doc
+ return [parts[0], curr_hash] if parts.size == 1
+ field = parts.pop # last one is the field
+ parts.each_with_index do |part, i|
+ part_accessor = curr_hash.kind_of?(Array) ? part.to_i : part
+ part_exists = curr_hash.kind_of?(Array) ? !curr_hash[part_accessor].nil? : curr_hash.has_key?(part_accessor)
+ return [part, curr_hash, i] if break_if_dne && !part_exists # !curr_hash.has_key?(part_accessor)
+ curr_hash[part_accessor] ||= {}
+ return [field, curr_hash[part_accessor], i+1] if parts.size == i+1
+ curr_hash = curr_hash[part_accessor]
+ end
+ end
+ private :hash_for_field
+
+
+ # Override this with your own validate() method for validations.
+ # Simply push your errors into the self.errors property and
+ # if self.errors remains empty your document will be valid.
+ # def validate
+ # self.errors.add "name", "cannot be blank"
+ # end
+ def validate
+ true
+ end
+ private :validate
+
+ # Class Methods
+ module ClassMethods
+ # Returns this models own db attribute if set, otherwise will return Mongomatic.db
+ def db
+ @db || Mongomatic.db || raise(ArgumentError, "No db supplied")
+ end
+
+ # Override Mongomatic.db with a Mongo::DB instance for this model specifically
+ # MyModel.db = Mongo::Connection.new().db('mydb_mymodel')
+ def db=(obj)
+ unless obj.is_a?(Mongo::DB)
+ raise(ArgumentError, "Must supply a Mongo::DB object")
+ end; @db = obj
+ end
+
+ # Override this method on your model if you want to use a different collection name
+ def collection_name
+ self.to_s.tableize
+ end
+
+ # Return the raw MongoDB collection for this model
+ def collection
+ @collection ||= self.db.collection(self.collection_name)
+ end
+
+ # Query MongoDB for documents. Same arguments as
+ # http://api.mongodb.org/ruby/current/Mongo/Collection.html#find-instance_method
+ def find(query={}, opts={})
+ Mongomatic::Cursor.new(self, collection.find(query, opts))
+ end
+
+ # Query MongoDB and return one document only. Same arguments as http://api.mongodb.org/ruby/current/Mongo/Collection.html#find_one-instance_method
+ def find_one(query={}, opts={})
+ return nil unless doc = self.collection.find_one(query, opts)
+ self.new(doc, false)
+ end
+
+ # Query MongoDB for existing document. If found, return existing or initialize a new object with the parameters
+ def find_or_initialize(query={}, opts={})
+ find_one(query, opts) || new(query, true)
+ end
+
+ # Same as Class.find
+ def all
+ find
+ end
+
+ # Return the number of documents in the collection
+ def count
+ collection.count
+ end
+
+ # Return the first document in the collection
+ def first
+ find.limit(1).next_document
+ end
+
+ # Is the collection empty? This method is much more efficient than doing Collection.count == 0
+ def empty?
+ find.limit(1).has_next? == false
+ end
+
+ # Iterate over all documents in the collection (uses a Mongomatic::Cursor)
+ def each
+ find.each { |found| yield(found) }
+ end
+
+ # Drop the collection. Calls the class method callbacks before_drop and after_drop
+ def drop
+ do_callback(:before_drop)
+ collection.drop
+ do_callback(:after_drop)
+ end
+
+ def insert(doc_hash, opts={})
+ d = new(doc_hash)
+ d.insert(opts)
+ end
+
+ def insert!(doc_hash, opts={})
+ insert(doc_hash, opts.merge(:safe => true))
+ end
+
+ def do_callback(meth)
+ return false unless respond_to?(meth, true)
+ send(meth)
+ end
+ private :do_callback
+ end
end
require "#{File.dirname(__FILE__)}/mongomatic/observer"
require "#{File.dirname(__FILE__)}/mongomatic/observable"
require "#{File.dirname(__FILE__)}/mongomatic/exceptions"
require "#{File.dirname(__FILE__)}/mongomatic/util"
-require "#{File.dirname(__FILE__)}/mongomatic/m_hash"
-require "#{File.dirname(__FILE__)}/mongomatic/cursor"
require "#{File.dirname(__FILE__)}/mongomatic/modifiers"
-require "#{File.dirname(__FILE__)}/mongomatic/errors"
require "#{File.dirname(__FILE__)}/mongomatic/expectations"
require "#{File.dirname(__FILE__)}/mongomatic/active_model_compliancy"
require "#{File.dirname(__FILE__)}/mongomatic/type_converters"
View
330 lib/mongomatic/base.rb
@@ -1,339 +1,11 @@
module Mongomatic
class Base
+ include Mongomatic
include Mongomatic::Modifiers
include Mongomatic::Util
include Mongomatic::ActiveModelCompliancy
include Mongomatic::TypedFields
- class << self
- # Returns this models own db attribute if set, otherwise will return Mongomatic.db
- def db
- @db || Mongomatic.db || raise(ArgumentError, "No db supplied")
- end
-
- # Override Mongomatic.db with a Mongo::DB instance for this model specifically
- # MyModel.db = Mongo::Connection.new().db('mydb_mymodel')
- def db=(obj)
- unless obj.is_a?(Mongo::DB)
- raise(ArgumentError, "Must supply a Mongo::DB object")
- end; @db = obj
- end
-
- # Override this method on your model if you want to use a different collection name
- def collection_name
- self.to_s.tableize
- end
-
- # Return the raw MongoDB collection for this model
- def collection
- @collection ||= self.db.collection(self.collection_name)
- end
-
- # Query MongoDB for documents. Same arguments as http://api.mongodb.org/ruby/current/Mongo/Collection.html#find-instance_method
- def find(query={}, opts={})
- Mongomatic::Cursor.new(self, collection.find(query, opts))
- end
-
- # Query MongoDB for existing document. If found, return existing or initialize a new object with the parameters
- def find_or_initialize(query={}, opts={})
- find_one(query, opts) || new(query, true)
- end
-
- # Query MongoDB and return one document only. Same arguments as http://api.mongodb.org/ruby/current/Mongo/Collection.html#find_one-instance_method
- def find_one(query={}, opts={})
- return nil unless doc = self.collection.find_one(query, opts)
- self.new(doc, false)
- end
-
- # Return a Mongomatic::Cursor instance of all documents in the collection.
- def all
- find
- end
-
- # Iterate over all documents in the collection (uses a Mongomatic::Cursor)
- def each
- find.each { |found| yield(found) }
- end
-
- # Return the first document in the collection
- def first
- find.limit(1).next_document
- end
-
- # Is the collection empty? This method is much more efficient than doing Collection.count == 0
- def empty?
- find.limit(1).has_next? == false
- end
-
- # Return the number of documents in the collection
- def count
- find.count
- end
-
- def drop
- do_callback(:before_drop)
- collection.drop
- do_callback(:after_drop)
- end
-
- def do_callback(meth)
- return false unless respond_to?(meth, true)
- send(meth)
- end
-
- def insert(doc_hash, opts={})
- d = new(doc_hash)
- d.insert(opts)
- end
-
- def insert!(doc_hash, opts={})
- insert(doc_hash, opts.merge(:safe => true))
- end
- end
-
- attr_accessor :removed, :is_new, :errors
-
- def initialize(doc_hash=Mongomatic::MHash.new, is_new=true)
- self.doc = doc_hash
- self.removed = false
- self.is_new = is_new
- self.errors = Mongomatic::Errors.new
- do_callback(:after_initialize)
- end
-
- def doc=(hash)
- hash = Mongomatic::MHash.new(hash) unless hash.is_a?(Mongomatic::MHash)
- @doc = hash
- end
-
- def doc
- @doc
- end
-
- # Override this with your own validate() method for validations.
- # Simply push your errors into the self.errors property and
- # if self.errors remains empty your document will be valid.
- # def validate
- # self.errors.add "name", "cannot be blank"
- # end
- def validate
- true
- end
-
- def valid?
- check_typed_fields!
- self.errors = Mongomatic::Errors.new
- do_callback(:before_validate)
- validate
- do_callback(:after_validate)
- self.errors.empty?
- end
-
- def new?
- self.is_new == true
- end
-
- def is_new?
- !!new?
- end
-
- # Set a field on this document:
- # mydoc["name"] = "Ben"
- # mydoc["address"] = { "city" => "San Francisco" }
- def []=(k,v)
- @doc[k.to_s] = v
- end
-
- # Returns true if document contains key
- def has_key?(key)
- field, hash = hash_for_field(key.to_s, true)
- hash.has_key?(field)
- end
-
- def set_value_for_key(key, value)
- field, hash = hash_for_field(key.to_s)
- hash[field] = value
- end
-
- def value_for_key(key)
- field, hash = hash_for_field(key.to_s, true)
- hash[field]
- end
-
- ##
- # Same as Hash#delete
- #
- # mydoc.delete("name")
- # => "Ben"
- # mydoc.has_hey?("name")
- # => false
- def delete(key)
- @doc.delete(key)
- end
-
- # Fetch a field (just like a hash):
- # mydoc["name"]
- # => "Ben"
- def [](k)
- @doc[k.to_s]
- end
-
- # Merge this document with the supplied hash. Useful for updates:
- # mydoc.merge(params[:user])
- def merge(hash)
- hash.each { |k,v| self[k] = v }; @doc
- end
-
- # Will return true if the document has been removed.
- def removed?
- self.removed == true
- end
-
- # Check equality with another Mongomatic document
- def ==(obj)
- obj.is_a?(self.class) && obj.doc["_id"] == @doc["_id"]
- end
-
- # Reload the document from the database
- def reload
- if obj = self.class.find({"_id" => @doc["_id"]}).next_document
- self.doc = obj.doc; true
- end
- end
-
- # If the document is new then an insert is performed, otherwise, an update is peformed.
- def save(opts={})
- (new?) ? insert(opts) : update(opts)
- end
-
- # Calls save(...) with {:safe => true} passed in as an option.
- def save!(opts={})
- save(opts.merge(:safe => true))
- end
-
- # Insert the document into the database. Will return false if the document has
- # already been inserted or is invalid. Returns the generated BSON::ObjectId
- # for the new document. Will silently fail if MongoDB is unable to insert the
- # document, use insert! or send in {:safe => true} if you want a Mongo::OperationError.
- # If you want to raise the following errors also, pass in {:raise => true}
- # * Raises Mongomatic::Exceptions::DocumentNotNew if document is not new
- # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
- def insert(opts={})
- if opts[:raise] == true
- raise Mongomatic::Exceptions::DocumentWasRemoved if removed?
- raise Mongomatic::Exceptions::DocumentNotNew unless new?
- raise Mongomatic::Exceptions::DocumentNotValid unless valid?
- else
- return false unless new? && valid?
- end
-
- do_callback(:before_insert)
- do_callback(:before_insert_or_update)
- if ret = self.class.collection.insert(@doc,opts)
- @doc["_id"] = @doc.delete(:_id) if @doc[:_id]
- self.is_new = false
- end
- do_callback(:after_insert)
- do_callback(:after_insert_or_update)
- ret
- end
-
- # Calls insert(...) with {:safe => true} passed in as an option.
- # * Raises Mongo::OperationError if there was a DB error on inserting
- # If you want to raise the following errors also, pass in {:raise => true}
- # * Raises Mongomatic::Exceptions::DocumentNotNew if document is not new
- # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
- def insert!(opts={})
- insert(opts.merge(:safe => true))
- end
-
- # Will persist any changes you have made to the document. Silently fails on
- # db update error. Use update! or pass in {:safe => true} to raise a
- # Mongo::OperationError if that's what you want.
- # If you want to raise the following errors also, pass in {:raise => true}
- # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
- # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
- # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been removed
- def update(opts={},update_doc=@doc)
- if opts[:raise] == true
- raise Mongomatic::Exceptions::DocumentWasRemoved if removed?
- raise Mongomatic::Exceptions::DocumentIsNew if new?
- raise Mongomatic::Exceptions::DocumentNotValid unless valid?
- else
- return false if new? || removed? || !valid?
- end
- do_callback(:before_update)
- do_callback(:before_insert_or_update)
- ret = self.class.collection.update({"_id" => @doc["_id"]}, update_doc, opts)
- do_callback(:after_update)
- do_callback(:after_insert_or_update)
- ret
- end
-
- # Calls update(...) with {:safe => true} passed in as an option.
- # * Raises Mongo::OperationError if there was a DB error on updating
- # If you want to raise the following errors also, pass in {:raise => true}
- # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
- # * Raises Mongomatic::Exceptions::DocumentNotValid if there are validation errors
- # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been removed
- def update!(opts={},update_doc=@doc)
- update(opts.merge(:safe => true),update_doc)
- end
-
- # Remove this document from the collection. Silently fails on db error,
- # use remove! or pass in {:safe => true} if you want an exception raised.
- # If you want to raise the following errors also, pass in {:raise => true}
- # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
- # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been already removed
- def remove(opts={})
- if opts[:raise] == true
- raise Mongomatic::Exceptions::DocumentWasRemoved if removed?
- raise Mongomatic::Exceptions::DocumentIsNew if new?
- else
- return false if new? || removed?
- end
- do_callback(:before_remove)
- if ret = self.class.collection.remove({"_id" => @doc["_id"]})
- self.removed = true; freeze; ret
- end
- do_callback(:after_remove)
- ret
- end
-
- # Calls remove(...) with {:safe => true} passed in as an option.
- # * Raises Mongo::OperationError if there was a DB error on removing
- # If you want to raise the following errors also, pass in {:raise => true}
- # * Raises Mongomatic::Exceptions::DocumentIsNew if document is new
- # * Raises Mongomatic::Exceptions::DocumentWasRemoved if document has been already removed
- def remove!(opts={})
- remove(opts.merge(:safe => true))
- end
-
- # Return this document as a hash.
- def to_hash
- @doc || {}
- end
-
- def hash_for_field(field, break_if_dne=false)
- parts = field.split(".")
- curr_hash = self.doc
- return [parts[0], curr_hash] if parts.size == 1
- field = parts.pop # last one is the field
- parts.each_with_index do |part, i|
- part_accessor = curr_hash.kind_of?(Array) ? part.to_i : part
- return [part, curr_hash] if break_if_dne && !curr_hash.has_key?(part_accessor)
- curr_hash[part_accessor] ||= {}
- return [field, curr_hash[part_accessor]] if parts.size == i+1
- curr_hash = curr_hash[part_accessor]
- end
- end
-
- def do_callback(meth)
- notify(meth) if self.class.included_modules.include?(Mongomatic::Observable) # TODO entire block is smelly, doesnt belong here
- return false unless respond_to?(meth, true)
- send(meth)
- end
-
def transaction(key=nil, duration=5, &block)
raise Mongomatic::Exceptions::DocumentIsNew if new?
if key.is_a?(Hash) && key[:scope]
View
403 spec/mongomatic/mongomatic_spec.rb
@@ -0,0 +1,403 @@
+require 'spec_helper'
+
+describe 'Mongomatic Base Module' do
+ before(:each) do
+ Person.collection.drop
+ end
+ describe "new instance" do
+ subject { Person.new }
+ it "is new" do
+ subject.should be_new
+ subject.is_new?.should be_true
+ end
+ it "is not removed" do
+ subject.should_not be_removed
+ end
+ it "has no errors" do
+ subject.errors.should be_empty
+ end
+ it "runs after_initialize" do
+ subject.called_callbacks.should include :after_initialize
+ end
+ context "with doc passed in" do
+ let(:initial_doc) { {:name => "Jordan"} }
+ subject { Person.new(initial_doc) }
+ it "is initialized with given doc" do
+ subject.doc == initial_doc
+ end
+ end
+ end
+ describe "hash methods" do
+ let(:name) { "Jordan"}
+ let(:city) { "San Francisco" }
+ let(:emp_name) { "Making Fun" }
+ let(:mm_proj) do
+ {:name => "Mongmatic",
+ :desc => "MongoDB ODM",
+ :contributors => [{:name => "Justin"}, {:name => "Jordan"} ,{:name => "Ben"}]}
+ end
+ subject do
+ Person.new(:name => name,
+ :city => city,
+ :employer => { :name => emp_name },
+ :projects => [mm_proj, {:name => "easy_tet", :desc => "Erlang Testing"}])
+ end
+ specify "equality operator" do
+ id = subject.insert
+ clone = Person.find_one(id)
+ clone.should == subject
+ end
+ describe "accessors" do
+ it "returns value of existing top level key (with string key)" do
+ subject['name'].should == name
+ end
+ it "returns value of existing top level key (with symbol key)" do
+ subject[:name].should == name
+ end
+ it "returns nil for non-existent key" do
+ subject['dne'].should be_nil
+ end
+ it "sets value for top level key" do
+ subject[:city] = "Chicago"
+ subject['city'].should == "Chicago"
+ end
+ it "returns value for nested key" do
+ subject['employer.name'].should == emp_name
+ end
+ it "returns nil for non-existent nested key" do
+ subject['employer.income'].should be_nil
+ end
+ it "sets value for nested key" do
+ subject['employer.position'] = "Engineer"
+ subject['employer.position'].should == "Engineer"
+ end
+ it "returns nested hash in array of subdocs" do
+ subject['projects.0']['name'].should == mm_proj[:name]
+ end
+ it "returns nil for non-existent index in array of subdocs that" do
+ subject['projects.3'].should be_nil
+ end
+ it "sets value at index in array of subdocs" do
+ subject['projects.3'] = {:name => "ht"}
+ subject['projects.3.name'].should == "ht"
+ end
+ it "returns value for key of doc in array of subdocs" do
+ subject['projects.0.name'].should == mm_proj[:name]
+ end
+ it "returns nil for for non-existent key for doc in array of subdocs" do
+ subject['projects.1.contributors'].should be_nil
+ end
+ it "sets value for key for doc in array of subdocs" do
+ subject['projects.1.name'] = "Some New Name"
+ subject['projects.1.name'].should == "Some New Name"
+ end
+ it "returns subdocument at deeply nested index" do
+ subject['projects.0.contributors.1'][:name].should == "Jordan"
+ end
+ it "returns nil for non-existent index in deeply nested array of subdocs" do
+ subject['projects.0.contributors.4'].should be_nil
+ end
+ it "sets subdocument at index in a deeply nested array of subdocs" do
+ subject['projects.0.contributors.3'] = {:name => "Some Dude"}
+ subject['projects.0.contributors.3.name'] = "Some Dude"
+ end
+ end
+ describe "#has_key?" do
+ specify "returns true when top-level key exists" do
+ subject.should have_key :name
+ subject.should have_key 'name'
+ end
+ specify "returns false when top-level key D.N.E" do
+ subject.should_not have_key :age
+ subject.should_not have_key 'age'
+ end
+ specify "returns true when nested key exists" do
+ subject.should have_key 'employer.name'
+ end
+ specify "returns false when nested key D.N.E" do
+ subject.should_not have_key 'employer.income'
+ end
+ specify "returns true when index in array of subdocs exists" do
+ subject.should have_key 'projects.0'
+ subject.should have_key 'projects.1'
+ end
+ specify "returns false when index in array of subdocs D.N.E" do
+ subject.should_not have_key 'projects.3'
+ end
+ specify "returns true if key exists in existing subdoc" do
+ subject.should have_key 'projects.1.name'
+ end
+ specify "returns false if key D.N.E. in existing subdoc" do
+ subject.should_not have_key "projects.1.contributors"
+ end
+ specify "returns false if subdoc does not exist when checking for subdoc key" do
+ subject.should_not have_key "projects.3.name"
+ end
+ specify "returns true for deeply nested index access to array" do
+ subject.should have_key "projects.0.contributors.1"
+ end
+ specify "returns false for non-existent deeply nested index access to array" do
+ subject.should_not have_key "projects.0.contributors.3"
+ end
+ specify "returns true for key in deeply nested subdoc" do
+ subject.should have_key "projects.0.contributors.1.name"
+ end
+ specify "returns false for non-existent key deeply nested in subdoc" do
+ subject.should_not have_key "projects.0.contributors.1.age"
+ end
+ end
+ describe "#delete" do
+ it "removes existing top-level key (using symbol)" do
+ subject.delete(:name)
+ subject['name'].should be_nil
+ end
+ it "removes existing top-level key (using string)" do
+ subject.delete('name')
+ subject[:name].should be_nil
+ end
+ it "is silent no-op on non-existent top-level key" do
+ subject.delete('age')
+ subject.delete(:age)
+ subject['age'].should be_nil
+ end
+ end
+ describe "#merge" do
+ before(:each) do
+ subject.merge(:name => "Jordan", :dog => "Nola")
+ end
+ it "updates existing keys" do
+ subject['name'].should == "Jordan"
+ end
+ it "creates new keys" do
+ subject['dog'].should == "Nola"
+ end
+ end
+ end
+ describe "find" do
+ it "returns a cursor" do
+ Person.find.should be_kind_of(Mongomatic::Cursor)
+ end
+ specify "#all is alias for find" do
+ Person.all.should be_kind_of(Mongomatic::Cursor)
+ end
+ context "when no documents exist" do
+ it "cursor is empty" do
+ Person.find.should be_empty
+ end
+ end
+ context "when documents exist" do
+ let(:docs) do
+ [{:name => "Jordan"}, {:name => "Ben"}, {:name => "Justin"}]
+ end
+ before(:each) do
+ docs.each do |doc|
+ Person.collection.insert(doc)
+ end
+ end
+ it "returns all docs if no query or opts are provided" do
+ Person.find.count.should == docs.count
+ end
+ it "returns only docs matching query" do
+ Person.find(:name => /^J/).count.should == 2
+ end
+ end
+ end
+ describe "find one" do
+ context "when document exists" do
+ subject { Person.new(:name => "Jordan", :age => 22) }
+ before(:each) do
+ @id = subject.insert
+ end
+ it "returns document when querying by id" do
+ Person.find_one(@id).should be_kind_of(Person)
+ end
+ it "returns document when querying by fields" do
+ Person.find_one(:name => "Jordan").should be_kind_of(Person)
+ end
+ it "returns only one document even with multiple matches" do
+ Person.new(:name => "John", :age => 22)
+ Person.find_one(:age => 22).should be_kind_of(Person)
+ end
+ end
+ context "when document D.N.E" do
+ it "returns nil" do
+ Person.find_one(:name => "Jordan").should be_nil
+ end
+ specify "find_or_intialize returns new document" do
+ Person.find_or_initialize(:name => "Jordan").should be_new
+ end
+ end
+ end
+ describe "convenience class methods" do
+ specify "Class.first when documents exist returns doc instance" do
+ Person.new(:name => "Jordan").insert
+ Person.first['name'].should == "Jordan"
+ end
+ specify "Class.first when no documents exists returns nil" do
+ Person.first.should be_nil
+ end
+ specify "Class.empty? returns true when no documents exist" do
+ Person.should be_empty
+ end
+ specify "Class.empty? returns false when docs exist" do
+ Person.insert(:name => "Jordan")
+ Person.should_not be_empty
+ end
+ specify "Class.count returns 0 when no docs exist" do
+ Person.count.should == 0
+ end
+ specify "Class.count returns number of existing docs" do
+ Person.insert(:name => "Jordan")
+ Person.insert(:name => "Justin")
+ Person.count.should == 2
+ end
+ end
+ describe "enumeration" do
+ before(:each) do
+ ["Jordan", "Justin", "Ben"].each do |n|
+ Person.new(:name => n).insert
+ end
+ end
+ specify "iterating over all documents" do
+ i = 0;
+ Person.each do |p|
+ p.should be_kind_of(Person)
+ i += 1
+ end
+ i.should == Person.collection.count
+ end
+ end
+ describe "unsafe operations" do
+ subject { Person.new(:name => "Jordan") }
+ describe "insert" do
+ context "valid document" do
+ before(:each) do
+ @id = subject.insert
+ end
+ it "returns Object Id" do
+ @id.should be_kind_of(BSON::ObjectId)
+ end
+ it "inserts the document" do
+ Person.collection.count.should == 1
+ end
+ it "calls before_insert" do
+ subject.called_callbacks.should include :before_insert
+ end
+ it "calls after_insert" do
+ subject.called_callbacks.should include :after_insert
+ end
+ it "calls before_insert_or_update" do
+ subject.called_callbacks.should include :before_insert_or_update
+ end
+ it "calls after_insert_or_update" do
+ subject.called_callbacks.should include :after_insert_or_update
+ end
+ it "stores the object id in the _id key of the document" do
+ subject.doc['_id'].should == @id
+ end
+ it "marks instance as no longer new" do
+ subject.should_not be_new
+ end
+ end
+ context "invalid document" do
+ subject { Person.new(:name => "Jordan") }
+ before(:each) do
+ subject.stub(:valid?, false)
+ end
+ it "returns false" do
+ subject.insert.should be_false
+ end
+ it "raises DocumentNotValid when passing :raise => true" do
+ expect { subject.insert(:raise => true) }.to raise_exception(Mongomatic::Exceptions::DocumentNotValid)
+ end
+ end
+ end
+ describe "update" do
+ before(:each) do
+ subject.insert
+ subject[:name] = "Justin"
+ subject.update
+ end
+ it "updates document" do
+ Person.find_one(subject[:_id])['name'].should == "Justin"
+ end
+ it "calls before_update" do
+ subject.called_callbacks.should include :before_update
+ end
+ it "calls after_update" do
+ subject.called_callbacks.should include :after_update
+ end
+ it "calls before_insert_or_update" do
+ subject.called_callbacks.select { |c| c == :before_insert_or_update }.count.should == 2
+ end
+ it "calls after_insert_or_update" do
+ subject.called_callbacks.select { |c| c == :after_insert_or_update }.count.should == 2
+ end
+ end
+ describe "save" do
+ it "inserts document" do
+ subject.save
+ subject.should_not be_new
+ end
+ it "updates document" do
+ subject.save
+ subject['name'] = "Some New Name"
+ subject.save
+ Person.find_one(subject['_id'])['name'].should == "Some New Name"
+ end
+ end
+ describe "remove" do
+ before(:each) do
+ @id = subject.insert
+ subject.remove
+ end
+ it "removes document" do
+ Person.find_one(@id).should be_nil
+ end
+ it "calls before_remove" do
+ subject.called_callbacks.should include :before_remove
+ end
+ it "calls after_remove" do
+ subject.called_callbacks.should include :after_remove
+ end
+ end
+ end
+ describe "safe operations" do
+ subject { Person.new(:name => "Jordan") }
+ before(:each) do
+ Person.collection.ensure_index([['name', Mongo::ASCENDING]], :unique => true)
+ subject.insert!
+ end
+ after(:each) do
+ Person.collection.drop_index('name_1')
+ end
+ describe "insert!" do
+ it "raises Mongo::Operation errors" do
+ expect { Person.insert!(:name => "Jordan") }.to raise_exception(Mongo::OperationFailure)
+ end
+ end
+ describe "update!" do
+ it "raises Mongo::Operation errors" do
+ Person.insert(:name => "John")
+ dup = Person.find_one(:name => "John")
+ dup['name'] = "Jordan"
+ expect { dup.update! }.to raise_exception(Mongo::OperationFailure)
+ end
+ end
+ describe "save!" do
+ it "raises Mongo::Operation errors" do
+ subject.save!
+ expect { Person.new(subject.doc).save! }.to raise_exception(Mongo::OperationFailure)
+ end
+ end
+ end
+ it "reloads data" do
+ p1 = Person.new(:name => "Jordan")
+ p1.insert
+ p2 = Person.find_one(:name => "Jordan")
+ p2['name'] = "John"
+ p2.update
+ p1.reload
+ p1['name'].should == "John"
+ end
+end
View
59 spec/spec_helper.rb
@@ -0,0 +1,59 @@
+require 'bundler/setup'
+require 'rspec'
+require 'mongomatic'
+
+Mongomatic.db = Mongo::Connection.new.db("mongomatic_test")
+
+class Person
+ include Mongomatic
+
+ def called_callbacks
+ @called_callbacks ||= []
+ end
+
+ def after_initialize
+ @called_callbacks ||= []
+ @called_callbacks << :after_initialize
+ end
+
+ def before_insert
+ @called_callbacks ||= []
+ @called_callbacks << :before_insert
+ end
+
+ def after_insert
+ @called_callbacks ||= []
+ @called_callbacks << :after_insert
+ end
+
+ def before_insert_or_update
+ @called_callbacks ||= []
+ @called_callbacks << :before_insert_or_update
+ end
+
+ def after_insert_or_update
+ @called_callbacks ||= []
+ @called_callbacks << :after_insert_or_update
+ end
+
+ def before_update
+ @called_callbacks ||= []
+ @called_callbacks << :before_update
+ end
+
+ def after_update
+ @called_callbacks ||= []
+ @called_callbacks << :after_update
+ end
+
+ def before_remove
+ @called_callbacks ||= []
+ @called_callbacks << :before_remove
+ end
+
+ def after_remove
+ @called_callbacks ||= []
+ @called_callbacks << :after_remove
+ end
+end
+

0 comments on commit e2cd0f7

Please sign in to comment.