diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 75459d1..02a6fe2 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -46,6 +46,7 @@ module CouchRest autoload :CastedModel, 'couchrest/more/casted_model' require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') + require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') # The CouchRest module methods handle the basic JSON serialization # and deserialization, as well as query parameters. The module also includes diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 953e125..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,24 +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 { |v| arr << v } - value = arr - elsif #{property.casted} - 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 44c73c3..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 @@ -68,7 +65,7 @@ module CouchRest 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, @@ -127,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/lib/couchrest/support/rails.rb b/lib/couchrest/support/rails.rb index 2fa267c..41957d4 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/support/rails.rb @@ -1,52 +1,53 @@ # This file contains various hacks for Rails compatibility. -# To use, just require in environment.rb, like so: -# -# require 'couchrest/support/rails' -class Hash - # Hack so that CouchRest::Document, which descends from Hash, - # doesn't appear to Rails routing as a Hash of options - def self.===(other) - return false if self == Hash && other.is_a?(CouchRest::Document) - super +if defined?(Rails) + + class Hash + # Hack so that CouchRest::Document, which descends from Hash, + # doesn't appear to Rails routing as a Hash of options + def self.===(other) + return false if self == Hash && other.is_a?(CouchRest::Document) + super + end end -end -CouchRest::Document.class_eval do - # Need this when passing doc to a resourceful route - alias_method :to_param, :id + CouchRest::Document.class_eval do + # Need this when passing doc to a resourceful route + alias_method :to_param, :id - # Hack so that CouchRest::Document, which descends from Hash, - # doesn't appear to Rails routing as a Hash of options - def is_a?(o) - return false if o == Hash - super - end - alias_method :kind_of?, :is_a? + # Hack so that CouchRest::Document, which descends from Hash, + # doesn't appear to Rails routing as a Hash of options + def is_a?(o) + return false if o == Hash + super + end + alias_method :kind_of?, :is_a? - # Gives extended doc a seamless logger - def logger - ActiveRecord::Base.logger + # Gives extended doc a seamless logger + def logger + ActiveRecord::Base.logger + end end -end -CouchRest::CastedModel.class_eval do - # The to_param method is needed for rails to generate resourceful routes. - # In your controller, remember that it's actually the id of the document. - def id - return nil if base_doc.nil? - base_doc.id + CouchRest::CastedModel.class_eval do + # The to_param method is needed for rails to generate resourceful routes. + # In your controller, remember that it's actually the id of the document. + def id + return nil if base_doc.nil? + base_doc.id + end + alias_method :to_param, :id end - alias_method :to_param, :id -end -require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors') + require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors') -CouchRest::Validation::ValidationErrors.class_eval do - # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. - # This method is called by error_messages_for - def count - errors.values.inject(0) { |error_count, errors_for_attribute| error_count + errors_for_attribute.size } + CouchRest::Validation::ValidationErrors.class_eval do + # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. + # This method is called by error_messages_for + def count + errors.values.inject(0) { |error_count, errors_for_attribute| error_count + errors_for_attribute.size } + end end -end + +end \ No newline at end of file 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 a1bbc43..293a66e 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -288,7 +288,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 @@ -298,6 +299,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 @@ -340,6 +342,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 @@ -347,6 +350,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"}