diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index e7a6303..bc83571 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -37,37 +37,55 @@ module CouchRest def cast_keys return unless self.class.properties self.class.properties.each do |property| - next unless property.casted - key = self.has_key?(property.name) ? property.name : property.name.to_sym - # Don't cast the property unless it has a value - next unless self[key] - target = property.type - if target.is_a?(Array) - klass = ::CouchRest.constantize(target[0]) - arr = self[key].collect do |value| - # Auto parse Time objects - obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value) - obj.casted_by = self if obj.respond_to?(:casted_by) - obj.document_saved = true if obj.respond_to?(:document_saved) - obj - end - self[property.name] = target[0] != 'String' ? CastedArray.new(arr) : arr - else - # Auto parse Time objects - self[property.name] = if ((property.init_method == 'new') && target == 'Time') - self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key] - else - # Let people use :send as a Time parse arg - klass = ::CouchRest.constantize(target) - klass.send(property.init_method, self[key].dup) - end - self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by) - self[property.name].document_saved = true if self[property.name].respond_to?(:document_saved) - end - self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by) + cast_property(property) end end + def cast_property(property) + return unless property.casted + key = self.has_key?(property.name) ? property.name : property.name.to_sym + # Don't cast the property unless it has a value + return unless self[key] + target = property.type + if target.is_a?(Array) + klass = ::CouchRest.constantize(target[0]) + arr = self[key].dup.collect do |value| + unless value.instance_of?(klass) + value = convert_property_value(property, klass, value) + end + associate_casted_to_parent(value) + value + end + self[key] = target[0] != 'String' ? CastedArray.new(arr) : arr + else + klass = ::CouchRest.constantize(target) + unless self[key].instance_of?(klass) + self[key] = convert_property_value(property, klass, self[property.name]) + end + associate_casted_to_parent(self[property.name]) + end + end + + def associate_casted_to_parent(casted) + casted.casted_by = self if casted.respond_to?(:casted_by) + casted.document_saved = true if casted.respond_to?(:document_saved) + end + + def convert_property_value(property, klass, value) + if ((property.init_method == 'new') && klass.to_s == 'Time') + value.is_a?(String) ? Time.parse(value.dup) : value + else + klass.send(property.init_method, value.dup) + end + end + + def cast_property_by_name(property_name) + return unless self.class.properties + property = self.class.properties.detect{|property| property.name == property_name} + return unless property + cast_property(property) + end + module ClassMethods def property(name, options={}) @@ -108,28 +126,17 @@ module CouchRest # defines the setter for the property (and optional aliases) def create_property_setter(property) - meth = property.name + property_name = property.name class_eval <<-EOS - def #{meth}=(value) - if #{property.casted} && value.is_a?(Array) - arr = CastedArray.new - arr.casted_by = self - value.each do |v| - obj = #{property.type}.new(v) - arr << obj - end - value = arr - elsif #{property.casted} - value = #{property.type}.new(v) - value.casted_by = self if value.respond_to?(:casted_by) - end - self['#{meth}'] = value + def #{property_name}=(value) + self['#{property_name}'] = value + cast_property_by_name('#{property_name}') end EOS if property.alias class_eval <<-EOS - alias #{property.alias.to_sym}= #{meth.to_sym}= + alias #{property.alias.to_sym}= #{property_name.to_sym}= EOS end end diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index bd7377f..a3e941b 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -51,9 +51,6 @@ module CouchRest end end - - - # Automatically set updated_at and created_at fields # on the document whenever saving occurs. CouchRest uses a pretty # decent time format by default. See Time#to_json @@ -62,16 +59,13 @@ module CouchRest property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) - def created_at=(ignored); end - def updated_at=(ignored); end - save_callback :before do |object| object['updated_at'] = Time.now object['created_at'] = object['updated_at'] if object.new? end EOS end - + # Name a method that will be called before the document is first saved, # which returns a string to be used for the document's _id. # Because CouchDB enforces a constraint that each id must be unique, @@ -130,10 +124,15 @@ module CouchRest # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are # missing. In case of error, no attributes are changed. def update_attributes_without_saving(hash) - hash.each do |k, v| + # remove attributes that cannot be updated, silently ignoring them + # which matches Rails behavior when, for instance, setting created_at. + # make a copy, we don't want to change arguments + attrs = hash.dup + %w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)} + attrs.each do |k, v| raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") end - hash.each do |k, v| + attrs.each do |k, v| self.send("#{k}=",v) end end diff --git a/spec/couchrest/more/casted_extended_doc_spec.rb b/spec/couchrest/more/casted_extended_doc_spec.rb index 51afd77..eef1541 100644 --- a/spec/couchrest/more/casted_extended_doc_spec.rb +++ b/spec/couchrest/more/casted_extended_doc_spec.rb @@ -43,16 +43,14 @@ describe "assigning a value to casted attribute after initializing an object" do @car.driver.should be_nil end - # Note that this isn't casting the attribute, it's just assigning it a value - # (see "should not cast attribute") it "should let you assign the value" do @car.driver = @driver @car.driver.name.should == 'Matt' end - it "should not cast attribute" do + it "should cast attribute" do @car.driver = JSON.parse(JSON.generate(@driver)) - @car.driver.should_not be_instance_of(Driver) + @car.driver.should be_instance_of(Driver) end end diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 029e294..a6c5aa3 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -284,7 +284,8 @@ describe CouchRest::CastedModel do before :each do reset_test_db! @cat = Cat.new(:name => 'Sockington') - @cat.favorite_toy = CatToy.new(:name => 'Catnip Ball') + @favorite_toy = CatToy.new(:name => 'Catnip Ball') + @cat.favorite_toy = @favorite_toy @cat.toys << CatToy.new(:name => 'Fuzzy Stick') end @@ -294,6 +295,7 @@ describe CouchRest::CastedModel do end it "should be true after assignment" do + @cat.should be_new @cat.favorite_toy.should be_new @cat.toys.first.should be_new end @@ -336,6 +338,7 @@ describe CouchRest::CastedModel do it "should reference the top document for" do @course.base_doc.should === @course + @professor.casted_by.should === @course @professor.base_doc.should === @course @cat.base_doc.should === @course @toy1.base_doc.should === @course @@ -343,6 +346,7 @@ describe CouchRest::CastedModel do end it "should call setter on top document" do + @toy1.base_doc.should_not be_nil @toy1.base_doc.title = 'Tom Foolery' @course.title.should == 'Tom Foolery' end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index d4100bf..403eedd 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -118,7 +118,22 @@ describe "ExtendedDocument" do @art.update_attributes_without_saving('date' => Time.now, :title => "super danger") @art['title'].should == "super danger" end - + it "should silently ignore _id" do + @art.update_attributes_without_saving('_id' => 'foobar') + @art['_id'].should_not == 'foobar' + end + it "should silently ignore _rev" do + @art.update_attributes_without_saving('_rev' => 'foobar') + @art['_rev'].should_not == 'foobar' + end + it "should silently ignore created_at" do + @art.update_attributes_without_saving('created_at' => 'foobar') + @art['created_at'].should_not == 'foobar' + end + it "should silently ignore updated_at" do + @art.update_attributes_without_saving('updated_at' => 'foobar') + @art['updated_at'].should_not == 'foobar' + end it "should also work using attributes= alias" do @art.respond_to?(:attributes=).should be_true @art.attributes = {'date' => Time.now, :title => "something else"}