From 36f69ec3324fdab8992cdfc964d11b32a253836d Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Mon, 8 Jun 2009 11:48:15 -1000 Subject: [PATCH 1/6] check if Rails exists for the rails support --- lib/couchrest/support/rails.rb | 78 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/couchrest/support/rails.rb b/lib/couchrest/support/rails.rb index 2fa267c..021a4f3 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/support/rails.rb @@ -1,52 +1,52 @@ # 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 From 31180f6bc7ba24ebbd4fa728a8291d4f5642b4f2 Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Mon, 8 Jun 2009 11:52:02 -1000 Subject: [PATCH 2/6] require the rails support --- lib/couchrest.rb | 1 + 1 file changed, 1 insertion(+) 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 From ffceaec57da1a63e477c3a422a9c20c947f39e2b Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Mon, 8 Jun 2009 16:34:21 -1000 Subject: [PATCH 3/6] add silent settings for created_at and updated_at, allows for mass assignment with semantics that match rails --- lib/couchrest/more/extended_document.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 44c73c3..bd7377f 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -62,6 +62,9 @@ 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? From 130fdd16dd160ef70e74ed7f08b28ceb17824c55 Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Mon, 8 Jun 2009 16:34:52 -1000 Subject: [PATCH 4/6] cosmetic cleanup --- lib/couchrest/support/rails.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/support/rails.rb b/lib/couchrest/support/rails.rb index 021a4f3..41957d4 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/support/rails.rb @@ -11,6 +11,7 @@ if defined?(Rails) end end + CouchRest::Document.class_eval do # Need this when passing doc to a resourceful route alias_method :to_param, :id @@ -49,4 +50,4 @@ if defined?(Rails) end end -end +end \ No newline at end of file From 209e36f61be9eb81ccffd86376d9e1302051ca4a Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Mon, 8 Jun 2009 16:35:26 -1000 Subject: [PATCH 5/6] cast values through setters to ensure validations are run after mass assignment, for example --- lib/couchrest/mixins/properties.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 953e125..e7a6303 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -114,9 +114,13 @@ module CouchRest if #{property.casted} && value.is_a?(Array) arr = CastedArray.new arr.casted_by = self - value.each { |v| arr << v } + 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 From 92d7fdb94d148a484e925ca85b2f88444ea3898d Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Tue, 9 Jun 2009 18:02:04 -1000 Subject: [PATCH 6/6] refactoring how casting works --- lib/couchrest/mixins/properties.rb | 95 ++++++++++--------- lib/couchrest/more/extended_document.rb | 17 ++-- .../more/casted_extended_doc_spec.rb | 6 +- spec/couchrest/more/casted_model_spec.rb | 6 +- spec/couchrest/more/extended_doc_spec.rb | 17 +++- 5 files changed, 82 insertions(+), 59 deletions(-) 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"}