require 'test_helper' class Grandparent include MongoMapper::EmbeddedDocument key :grandparent, String end class Parent < Grandparent include MongoMapper::EmbeddedDocument key :parent, String end class Child < Parent include MongoMapper::EmbeddedDocument key :child, String end module KeyOverride def other_child read_attribute(:other_child) || "special result" end def other_child=(value) super(value + " modified") end end class OtherChild < Parent include MongoMapper::EmbeddedDocument include KeyOverride key :other_child, String end class EmbeddedDocumentTest < Test::Unit::TestCase context "Including MongoMapper::EmbeddedDocument in a class" do setup do @klass = Class.new do include MongoMapper::EmbeddedDocument end end should "add _id key" do @klass.keys['_id'].should_not be_nil end context "#to_mongo" do should "be nil if nil" do @klass.to_mongo(nil).should be_nil end should "convert to_mongo for other values" do doc = @klass.new(:foo => 'bar') to_mongo = @klass.to_mongo(doc) to_mongo.is_a?(Hash).should be_true to_mongo['foo'].should == 'bar' end end context "#from_mongo" do should "be nil if nil" do @klass.from_mongo(nil).should be_nil end should "be instance if instance of class" do doc = @klass.new @klass.from_mongo(doc).should == doc end should "be instance if hash of attributes" do doc = @klass.from_mongo({:foo => 'bar'}) doc.instance_of?(@klass).should be_true doc.foo.should == 'bar' end end end context "parent_model" do should "be nil if none of parents ancestors include EmbeddedDocument" do parent = Class.new document = Class.new(parent) do include MongoMapper::EmbeddedDocument end document.parent_model.should be_nil end should "work when other modules have been included" do grandparent = Class.new parent = Class.new grandparent do include MongoMapper::EmbeddedDocument end example_module = Module.new document = Class.new(parent) do include MongoMapper::EmbeddedDocument include example_module end document.parent_model.should == parent end should "find parent" do Parent.parent_model.should == Grandparent Child.parent_model.should == Parent end end context "defining a key" do setup do @document = Class.new do include MongoMapper::EmbeddedDocument end end should "work with name" do key = @document.key(:name) key.name.should == 'name' end should "work with name and type" do key = @document.key(:name, String) key.name.should == 'name' key.type.should == String end should "work with name, type and options" do key = @document.key(:name, String, :required => true) key.name.should == 'name' key.type.should == String key.options[:required].should be_true end should "work with name and options" do key = @document.key(:name, :required => true) key.name.should == 'name' key.options[:required].should be_true end should "be tracked per document" do @document.key(:name, String) @document.key(:age, Integer) @document.keys['name'].name.should == 'name' @document.keys['name'].type.should == String @document.keys['age'].name.should == 'age' @document.keys['age'].type.should == Integer end should "not be redefinable" do @document.key(:foo, String) @document.keys['foo'].type.should == String @document.key(:foo, Integer) @document.keys['foo'].type.should == String end should "create reader method" do @document.new.should_not respond_to(:foo) @document.key(:foo, String) @document.new.should respond_to(:foo) end should "create reader before typecast method" do @document.new.should_not respond_to(:foo_before_typecast) @document.key(:foo, String) @document.new.should respond_to(:foo_before_typecast) end should "create writer method" do @document.new.should_not respond_to(:foo=) @document.key(:foo, String) @document.new.should respond_to(:foo=) end should "create boolean method" do @document.new.should_not respond_to(:foo?) @document.key(:foo, String) @document.new.should respond_to(:foo?) end end context "keys" do should "be inherited" do Grandparent.keys.keys.sort.should == ['_id', 'grandparent'] Parent.keys.keys.sort.should == ['_id', 'grandparent', 'parent'] Child.keys.keys.sort.should == ['_id', 'child', 'grandparent', 'parent'] end should "propogate to subclasses if key added after class definition" do Grandparent.key :_type, String Grandparent.keys.keys.sort.should == ['_id', '_type', 'grandparent'] Parent.keys.keys.sort.should == ['_id', '_type', 'grandparent', 'parent'] Child.keys.keys.sort.should == ['_id', '_type', 'child', 'grandparent', 'parent'] end should "not add anonymous objects to the ancestor tree" do OtherChild.ancestors.any? { |a| a.name.blank? }.should be_false end should "not include descendant keys" do lambda { Parent.new.other_child }.should raise_error end end context "#inspect" do setup do @document = Class.new do include MongoMapper::Document key :animals end end should "call inspect on the document's attributes instead of to_s" do doc = @document.new doc.animals = %w(dog cat) doc.inspect.should include(%(animals: ["dog", "cat"])) end end context "subclasses" do should "default to nil" do Child.subclasses.should be_nil end should "be recorded" do Grandparent.subclasses.should == [Parent] Parent.subclasses.should == [Child, OtherChild] end end context "Applying default values for keys" do setup do @document = Class.new do include MongoMapper::EmbeddedDocument key :name, String, :default => 'foo' key :age, Integer, :default => 20 key :net_worth, Float, :default => 100.00 key :active, Boolean, :default => true key :smart, Boolean, :default => false key :skills, Array, :default => [1] key :options, Hash, :default => {'foo' => 'bar'} end @doc = @document.new end should "work for strings" do @doc.name.should == 'foo' end should "work for integers" do @doc.age.should == 20 end should "work for floats" do @doc.net_worth.should == 100.00 end should "work for booleans" do @doc.active.should == true @doc.smart.should == false end should "work for arrays" do @doc.skills.should == [1] @doc.skills << 2 @doc.skills.should == [1, 2] end should "work for hashes" do @doc.options['foo'].should == 'bar' @doc.options['baz'] = 'wick' @doc.options['baz'].should == 'wick' end end context "An instance of an embedded document" do setup do @document = Class.new do include MongoMapper::EmbeddedDocument key :name, String key :age, Integer end end should "automatically have an _id key" do @document.keys.keys.should include('_id') end should "have id method that sets _id" do doc = @document.new doc.id.should == doc._id.to_s end should "have a nil _root_document" do @document.new._root_document.should be_nil end context "setting custom id" do should "set _id" do doc = @document.new(:id => '1234') doc._id.should == '1234' end should "know that custom id is set" do doc = @document.new doc.using_custom_id?.should be_false doc.id = '1234' doc.using_custom_id?.should be_true end end context "being initialized" do should "accept a hash that sets keys and values" do doc = @document.new(:name => 'John', :age => 23) doc.attributes.keys.sort.should == ['_id', 'age', 'name'] doc.attributes['name'].should == 'John' doc.attributes['age'].should == 23 end should "be able to assign keys dynamically" do doc = @document.new(:name => 'John', :skills => ['ruby', 'rails']) doc.name.should == 'John' doc.skills.should == ['ruby', 'rails'] end should "set the root on embedded documents" do document = Class.new(@document) do many :children end doc = document.new :_root_document => 'document', 'children' => [{}] doc.children.first._root_document.should == 'document' end should "not throw error if initialized with nil" do lambda { @document.new(nil) }.should_not raise_error end end context "initialized when _type key present" do setup do ::FooBar = Class.new do include MongoMapper::EmbeddedDocument key :_type, String end end teardown do Object.send(:remove_const, :FooBar) end should "set _type to class name" do FooBar.new._type.should == 'FooBar' end should "not change _type if already set" do FooBar.new(:_type => 'Foo')._type.should == 'Foo' end end context "mass assigning keys" do should "update values for keys provided" do doc = @document.new(:name => 'foobar', :age => 10) doc.attributes = {:name => 'new value', :age => 5} doc.attributes[:name].should == 'new value' doc.attributes[:age].should == 5 end should "not update values for keys that were not provided" do doc = @document.new(:name => 'foobar', :age => 10) doc.attributes = {:name => 'new value'} doc.attributes[:name].should == 'new value' doc.attributes[:age].should == 10 end should "not ignore keys that have methods defined" do @document.class_eval do attr_writer :password def passwd @password end end doc = @document.new(:name => 'foobar', :password => 'secret') doc.passwd.should == 'secret' end should "typecast key values" do doc = @document.new(:name => 1234, :age => '21') doc.name.should == '1234' doc.age.should == 21 end end context "attributes" do should "default to hash with all keys" do doc = @document.new doc.attributes.keys.sort.should == ['_id', 'age', 'name'] end should "return all keys with values" do doc = @document.new(:name => 'string', :age => nil) doc.attributes.keys.sort.should == ['_id', 'age', 'name'] doc.attributes.values.should include('string') doc.attributes.values.should include(nil) end end context "to_mongo" do should "default to hash with _id key" do doc = @document.new doc.to_mongo.keys.should == ['_id'] end should "return all keys with non nil values" do doc = @document.new(:name => 'string', :age => nil) doc.to_mongo.keys.sort.should == ['_id', 'name'] doc.to_mongo.values.should include('string') doc.to_mongo.values.should_not include(nil) end end should "convert dates into times" do document = Class.new(@document) do key :start_date, Date end doc = document.new :start_date => "12/05/2009" doc.start_date.should == Date.new(2009, 12, 05) end context "clone" do should "regenerate the id" do doc = @document.new(:name => "foo", :age => 27) doc_id = doc.id clone = doc.clone clone_id = clone.id clone_id.should_not == doc_id end should "copy the attributes" do doc = @document.new(:name => "foo", :age => 27) clone = doc.clone clone.name.should == "foo" clone.age.should == 27 end end context "key shorcut access" do should "be able to read key with []" do doc = @document.new(:name => 'string') doc[:name].should == 'string' end context "[]=" do should "write key value for existing key" do doc = @document.new doc[:name] = 'string' doc[:name].should == 'string' end should "create key and write value for missing key" do doc = @document.new doc[:foo] = 'string' @document.keys.keys.include?('foo').should be_true doc[:foo].should == 'string' end end end context "indifferent access" do should "be enabled for keys" do doc = @document.new(:name => 'string') doc.attributes[:name].should == 'string' doc.attributes['name'].should == 'string' end end context "reading an attribute" do should "work for defined keys" do doc = @document.new(:name => 'string') doc.name.should == 'string' end should "raise no method error for undefined keys" do doc = @document.new lambda { doc.fart }.should raise_error(NoMethodError) end should "be accessible for use in the model" do @document.class_eval do def name_and_age "#{read_attribute(:name)} (#{read_attribute(:age)})" end end doc = @document.new(:name => 'John', :age => 27) doc.name_and_age.should == 'John (27)' end should "set instance variable" do @document.key :foo, Array doc = @document.new doc.instance_variable_get("@foo").should be_nil doc.foo doc.instance_variable_get("@foo").should == [] end should "not set instance variable if frozen" do @document.key :foo, Array doc = @document.new doc.instance_variable_get("@foo").should be_nil doc.freeze doc.foo doc.instance_variable_get("@foo").should be_nil end should "be overrideable by modules" do @document = Class.new do include MongoMapper::Document key :other_child, String end child = @document.new child.other_child.should be_nil @document.send :include, KeyOverride overriden_child = @document.new overriden_child.other_child.should == 'special result' end end context "reading an attribute before typcasting" do should "work for defined keys" do doc = @document.new(:name => 12) doc.name_before_typecast.should == 12 end should "raise no method error for undefined keys" do doc = @document.new lambda { doc.foo_before_typecast }.should raise_error(NoMethodError) end should "be accessible for use in a document" do @document.class_eval do def untypcasted_name read_attribute_before_typecast(:name) end end doc = @document.new(:name => 12) doc.name.should == '12' doc.untypcasted_name.should == 12 end end context "writing an attribute" do should "work for defined keys" do doc = @document.new doc.name = 'John' doc.name.should == 'John' end should "raise no method error for undefined keys" do doc = @document.new lambda { doc.fart = 'poof!' }.should raise_error(NoMethodError) end should "typecast value" do doc = @document.new doc.name = 1234 doc.name.should == '1234' doc.age = '21' doc.age.should == 21 end should "be accessible for use in the model" do @document.class_eval do def name_and_age=(new_value) new_value.match(/([^\(\s]+) \((.*)\)/) write_attribute :name, $1 write_attribute :age, $2 end end doc = @document.new doc.name_and_age = 'Frank (62)' doc.name.should == 'Frank' doc.age.should == 62 end should "be overrideable by modules" do @document = Class.new do include MongoMapper::Document key :other_child, String end child = @document.new(:other_child => 'foo') child.other_child.should == 'foo' @document.send :include, KeyOverride overriden_child = @document.new(:other_child => 'foo') overriden_child.other_child.should == 'foo modified' end end # writing an attribute context "checking if an attributes value is present" do should "work for defined keys" do doc = @document.new doc.name?.should be_false doc.name = 'John' doc.name?.should be_true end should "raise no method error for undefined keys" do doc = @document.new lambda { doc.fart? }.should raise_error(NoMethodError) end end context "equality" do should "be equal if id and class are the same" do (@document.new('_id' => 1) == @document.new('_id' => 1)).should be(true) end should "not be equal if class same but id different" do (@document.new('_id' => 1) == @document.new('_id' => 2)).should be(false) end should "not be equal if id same but class different" do @another_document = Class.new do include MongoMapper::Document end (@document.new('_id' => 1) == @another_document.new('_id' => 1)).should be(false) end end end # instance of a embedded document end