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"}