From 704d0a09bd7cbd79107ebd9ecf97637cfb1f0e40 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Wed, 27 May 2009 13:24:25 -0700 Subject: [PATCH 01/12] Added attributes= to casted model and extended doc --- lib/couchrest/more/casted_model.rb | 12 +++++++++ lib/couchrest/more/extended_document.rb | 1 + spec/couchrest/more/casted_model_spec.rb | 34 ++++++++++++++++++++++++ spec/couchrest/more/extended_doc_spec.rb | 6 +++++ 4 files changed, 53 insertions(+) diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index 6f442c8..03f441f 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -25,5 +25,17 @@ module CouchRest def [] key super(key.to_s) end + + # Sets the attributes from a hash + def update_attributes_without_saving(hash) + hash.each do |k, v| + raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") + end + hash.each do |k, v| + self.send("#{k}=",v) + end + end + alias :attributes= :update_attributes_without_saving + end end \ No newline at end of file diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 4fe8182..661330c 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -113,6 +113,7 @@ module CouchRest self.send("#{k}=",v) end end + alias :attributes= :update_attributes_without_saving # Takes a hash as argument, and applies the values by using writer methods # for each key. Raises a NoMethodError if the corresponding methods are diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 75b0c57..0f90360 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -4,6 +4,7 @@ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'person') +require File.join(FIXTURE_PATH, 'more', 'question') class WithCastedModelMixin < Hash @@ -106,7 +107,40 @@ describe CouchRest::CastedModel do @obj.keywords.should be_an_instance_of(Array) @obj.keywords.first.should == 'couch' end + end + + describe "update attributes without saving" do + before(:each) do + @question = Question.new(:q => "What is your quest?", :a => "To seek the Holy Grail") + end + it "should work for attribute= methods" do + @question.q.should == "What is your quest?" + @question['a'].should == "To seek the Holy Grail" + @question.update_attributes_without_saving(:q => "What is your favorite color?", 'a' => "Blue") + @question['q'].should == "What is your favorite color?" + @question.a.should == "Blue" + end + it "should also work for attributes= alias" do + @question.respond_to?(:attributes=).should be_true + @question.attributes = {:q => "What is your favorite color?", 'a' => "Blue"} + @question['q'].should == "What is your favorite color?" + @question.a.should == "Blue" + end + + it "should flip out if an attribute= method is missing" do + lambda { + @q.update_attributes_without_saving('foo' => "something", :a => "No green") + }.should raise_error(NoMethodError) + end + + it "should not change any attributes if there is an error" do + lambda { + @q.update_attributes_without_saving('foo' => "something", :a => "No green") + }.should raise_error(NoMethodError) + @question.q.should == "What is your quest?" + @question.a.should == "To seek the Holy Grail" + end end describe "saved document with casted models" do diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 6fa886f..66000a0 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -109,6 +109,12 @@ describe "ExtendedDocument" do @art['title'].should == "super danger" end + it "should also work using attributes= alias" do + @art.respond_to?(:attributes=).should be_true + @art.attributes = {'date' => Time.now, :title => "something else"} + @art['title'].should == "something else" + end + it "should flip out if an attribute= method is missing" do lambda { @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") From 9a026997dd70d468327fd784e340ea0a676e3d14 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 28 May 2009 12:18:23 -0700 Subject: [PATCH 02/12] valid? now recursively checks casted models. Added better validation spec coverage. --- lib/couchrest/mixins/validation.rb | 33 ++++----------- spec/couchrest/more/casted_model_spec.rb | 52 +++++++++++++++++++++++- spec/couchrest/more/extended_doc_spec.rb | 46 +++++++++++++++++++++ spec/fixtures/more/cat.rb | 1 + 4 files changed, 106 insertions(+), 26 deletions(-) diff --git a/lib/couchrest/mixins/validation.rb b/lib/couchrest/mixins/validation.rb index 82c8ab5..dda708e 100644 --- a/lib/couchrest/mixins/validation.rb +++ b/lib/couchrest/mixins/validation.rb @@ -115,8 +115,7 @@ module CouchRest # Check if a resource is valid in a given context # def valid?(context = :default) - result = self.class.validators.execute(context, self) - result && validate_casted_arrays + recursive_valid?(self, context, true) end # checking on casted objects @@ -133,29 +132,22 @@ module CouchRest result end - # Begin a recursive walk of the model checking validity - # - def all_valid?(context = :default) - recursive_valid?(self, context, true) - end - # Do recursive validity checking # def recursive_valid?(target, context, state) valid = state - target.instance_variables.each do |ivar| - ivar_value = target.instance_variable_get(ivar) - if ivar_value.validatable? - valid = valid && recursive_valid?(ivar_value, context, valid) - elsif ivar_value.respond_to?(:each) - ivar_value.each do |item| + target.each do |key, prop| + if prop.is_a?(Array) + prop.each do |item| if item.validatable? - valid = valid && recursive_valid?(item, context, valid) + valid = recursive_valid?(item, context, valid) && valid end end + elsif prop.validatable? + valid = recursive_valid?(prop, context, valid) && valid end end - return valid && target.valid? + target.class.validators.execute(context, target) && valid end @@ -218,15 +210,6 @@ module CouchRest end # end EOS end - - all = "all_valid_for_#{context.to_s}?" # all_valid_for_signup? - if !self.instance_methods.include?(all) - class_eval <<-EOS, __FILE__, __LINE__ - def #{all} # def all_valid_for_signup? - all_valid?('#{context.to_s}'.to_sym) # all_valid?('signup'.to_sym) - end # end - EOS - end end # Create a new validator of the given klazz and push it onto the diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 0f90360..43524a8 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -205,7 +205,57 @@ describe CouchRest::CastedModel do cat.masters.push Person.new cat.should be_valid end - end + describe "calling valid?" do + before :each do + @cat = Cat.new + @toy1 = CatToy.new + @toy2 = CatToy.new + @toy3 = CatToy.new + @cat.favorite_toy = @toy1 + @cat.toys << @toy2 + @cat.toys << @toy3 + end + + describe "on the top document" do + it "should put errors on all invalid casted models" do + @cat.should_not be_valid + @cat.errors.should_not be_empty + @toy1.errors.should_not be_empty + @toy2.errors.should_not be_empty + @toy3.errors.should_not be_empty + end + + it "should not put errors on valid casted models" do + @toy1.name = "Feather" + @toy2.name = "Twine" + @cat.should_not be_valid + @cat.errors.should_not be_empty + @toy1.errors.should be_empty + @toy2.errors.should be_empty + @toy3.errors.should_not be_empty + end + end + + describe "on a casted model property" do + it "should only validate itself" do + @toy1.should_not be_valid + @toy1.errors.should_not be_empty + @cat.errors.should be_empty + @toy2.errors.should be_empty + @toy3.errors.should be_empty + end + end + + describe "on a casted model inside a casted collection" do + it "should only validate itself" do + @toy2.should_not be_valid + @toy2.errors.should_not be_empty + @cat.errors.should be_empty + @toy1.errors.should be_empty + @toy3.errors.should be_empty + end + end + end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 66000a0..dcec051 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -1,6 +1,7 @@ require File.dirname(__FILE__) + '/../../spec_helper' require File.join(FIXTURE_PATH, 'more', 'article') require File.join(FIXTURE_PATH, 'more', 'course') +require File.join(FIXTURE_PATH, 'more', 'cat') describe "ExtendedDocument" do @@ -561,4 +562,49 @@ describe "ExtendedDocument" do @doc.other_arg.should == "foo-foo" end end + + describe "recursive validation on an extended document" do + before :each do + reset_test_db! + @cat = Cat.new(:name => 'Sockington') + end + + it "should not save if a nested casted model is invalid" do + @cat.favorite_toy = CatToy.new + @cat.should_not be_valid + @cat.save.should be_false + lambda{@cat.save!}.should raise_error + end + + it "should save when nested casted model is valid" do + @cat.favorite_toy = CatToy.new(:name => 'Squeaky') + @cat.should be_valid + @cat.save.should be_true + lambda{@cat.save!}.should_not raise_error + end + + it "should not save when nested collection contains an invalid casted model" do + @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new] + @cat.should_not be_valid + @cat.save.should be_false + lambda{@cat.save!}.should raise_error + end + + it "should save when nested collection contains valid casted models" do + @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')] + @cat.should be_valid + @cat.save.should be_true + lambda{@cat.save!}.should_not raise_error + end + + it "should not fail if the nested casted model doesn't have validation" do + Cat.property :trainer, :cast_as => 'Person' + Cat.validates_present :name + cat = Cat.new(:name => 'Mr Bigglesworth') + cat.trainer = Person.new + cat.trainer.validatable?.should be_false + cat.should be_valid + cat.save.should be_true + end + end end diff --git a/spec/fixtures/more/cat.rb b/spec/fixtures/more/cat.rb index 54abad5..a3cb054 100644 --- a/spec/fixtures/more/cat.rb +++ b/spec/fixtures/more/cat.rb @@ -6,6 +6,7 @@ class Cat < CouchRest::ExtendedDocument property :name property :toys, :cast_as => ['CatToy'], :default => [] + property :favorite_toy, :cast_as => 'CatToy' end class CatToy < Hash From 23341f36984ae0110640bf691620c9202fc40e8a Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 28 May 2009 16:09:53 -0700 Subject: [PATCH 03/12] Added new_model? and new_record? alias to casted model for rails compatibility. --- lib/couchrest/mixins/properties.rb | 2 ++ lib/couchrest/more/casted_model.rb | 8 +++++ lib/couchrest/more/extended_document.rb | 18 +++++++++++ spec/couchrest/more/casted_model_spec.rb | 41 ++++++++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 19d33d1..676f045 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -48,6 +48,7 @@ module CouchRest # 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 else @@ -60,6 +61,7 @@ module CouchRest 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 end end diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index 03f441f..bed9c7b 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -6,6 +6,7 @@ module CouchRest def self.included(base) base.send(:include, CouchRest::Mixins::Properties) base.send(:attr_accessor, :casted_by) + base.send(:attr_accessor, :document_saved) end def initialize(keys={}) @@ -26,6 +27,13 @@ module CouchRest super(key.to_s) end + # True if the casted model has already + # been saved in the containing document + def new_model? + !@document_saved + end + alias :new_record? :new_model? + # Sets the attributes from a hash def update_attributes_without_saving(hash) hash.each do |k, v| diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 4d08db5..dfe4f0d 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -201,6 +201,7 @@ module CouchRest raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database set_unique_id if new_document? && self.respond_to?(:set_unique_id) result = database.save_doc(self, bulk) + mark_as_saved if result["ok"] == true result["ok"] == true end @@ -226,5 +227,22 @@ module CouchRest end end + protected + + # Set document_saved flag on all casted models to true + def mark_as_saved + self.each do |key, prop| + if prop.is_a?(Array) + prop.each do |item| + if item.respond_to?(:document_saved) + item.send(:document_saved=, true) + end + end + elsif prop.respond_to?(:document_saved) + prop.send(:document_saved=, true) + end + end + end + end end diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 43524a8..b493656 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -258,4 +258,45 @@ describe CouchRest::CastedModel do end end end + + describe "calling new_model? on a casted model" do + before :each do + reset_test_db! + @cat = Cat.new(:name => 'Sockington') + @cat.favorite_toy = CatToy.new(:name => 'Catnip Ball') + @cat.toys << CatToy.new(:name => 'Fuzzy Stick') + end + + it "should be true on new" do + CatToy.new.new_model?.should be_true + CatToy.new.new_record?.should be_true + end + + it "should be true after assignment" do + @cat.favorite_toy.new_model?.should be_true + @cat.toys.first.new_model?.should be_true + end + + it "should not be true after create or save" do + @cat.create + @cat.save + @cat.favorite_toy.new_model?.should be_false + @cat.toys.first.new_model?.should be_false + end + + it "should not be true after get from the database" do + @cat.save + @cat = Cat.get(@cat.id) + @cat.favorite_toy.new_model?.should be_false + @cat.toys.first.new_model?.should be_false + end + + it "should still be true after a failed create or save" do + @cat.name = nil + @cat.create.should be_false + @cat.save.should be_false + @cat.favorite_toy.new_model?.should be_true + @cat.toys.first.new_model?.should be_true + end + end end From 3e4c90f104576afda6dd84b14123d95ca43296fe Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 28 May 2009 17:00:06 -0700 Subject: [PATCH 04/12] Fixed a comment --- lib/couchrest/more/casted_model.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index bed9c7b..2811de3 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -27,7 +27,7 @@ module CouchRest super(key.to_s) end - # True if the casted model has already + # False if the casted model has already # been saved in the containing document def new_model? !@document_saved @@ -46,4 +46,4 @@ module CouchRest alias :attributes= :update_attributes_without_saving end -end \ No newline at end of file +end From efeb654114dede387746b4670ef685623b4419ee Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 28 May 2009 17:56:42 -0700 Subject: [PATCH 05/12] casted_by is now set on assignment to a document. --- lib/couchrest/mixins/properties.rb | 12 ++++- lib/couchrest/more/property.rb | 19 ++++++++ spec/couchrest/more/property_spec.rb | 65 ++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 676f045..ae18323 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -44,13 +44,14 @@ module CouchRest target = property.type if target.is_a?(Array) klass = ::CouchRest.constantize(target[0]) - self[property.name] = self[key].collect do |value| + 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') @@ -63,6 +64,7 @@ module CouchRest 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) end end @@ -109,6 +111,14 @@ module CouchRest meth = 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 end EOS diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index 77e2b90..dccc7dd 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -38,3 +38,22 @@ module CouchRest end end + +class CastedArray < Array + attr_accessor :casted_by + + def << obj + obj.casted_by = self.casted_by if obj.respond_to?(:casted_by) + super(obj) + end + + def push(obj) + obj.casted_by = self.casted_by if obj.respond_to?(:casted_by) + super(obj) + end + + def []= index, obj + obj.casted_by = self.casted_by if obj.respond_to?(:casted_by) + super(index, obj) + end +end \ No newline at end of file diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 146f0f8..57af71a 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -3,6 +3,7 @@ require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'invoice') require File.join(FIXTURE_PATH, 'more', 'service') require File.join(FIXTURE_PATH, 'more', 'event') +require File.join(FIXTURE_PATH, 'more', 'cat') describe "ExtendedDocument properties" do @@ -131,6 +132,70 @@ describe "ExtendedDocument properties" do @event['occurs_at'].should be_an_instance_of(Time) end end + end +end + +describe "a newly created casted model" do + before(:each) do + reset_test_db! + @cat = Cat.new(:name => 'Toonces') + @squeaky_mouse = CatToy.new(:name => 'Squeaky') end + describe "assigned assigned to a casted property" do + it "should have casted_by set to its parent" do + @squeaky_mouse.casted_by.should be_nil + @cat.favorite_toy = @squeaky_mouse + @squeaky_mouse.casted_by.should === @cat + end + end + + describe "appended to a casted collection" do + it "should have casted_by set to its parent" do + @squeaky_mouse.casted_by.should be_nil + @cat.toys << @squeaky_mouse + @squeaky_mouse.casted_by.should === @cat + @cat.save + @cat.toys.first.casted_by.should === @cat + end + end + + describe "list assigned to a casted collection" do + it "should have casted_by set on all elements" do + toy1 = CatToy.new(:name => 'Feather') + toy2 = CatToy.new(:name => 'Mouse') + @cat.toys = [toy1, toy2] + toy1.casted_by.should === @cat + toy2.casted_by.should === @cat + @cat.save + @cat = Cat.get(@cat.id) + @cat.toys[0].casted_by.should === @cat + @cat.toys[1].casted_by.should === @cat + end + end end + +describe "a casted model retrieved from the database" do + before(:each) do + reset_test_db! + @cat = Cat.new(:name => 'Stimpy') + @cat.favorite_toy = CatToy.new(:name => 'Stinky') + @cat.toys << CatToy.new(:name => 'Feather') + @cat.toys << CatToy.new(:name => 'Mouse') + @cat.save + @cat = Cat.get(@cat.id) + end + + describe "as a casted property" do + it "should already be casted_by its parent" do + @cat.favorite_toy.casted_by.should === @cat + end + end + + describe "from a casted collection" do + it "should already be casted_by its parent" do + @cat.toys[0].casted_by.should === @cat + @cat.toys[1].casted_by.should === @cat + end + end +end \ No newline at end of file From d012380b673561660601131b22a845bd8c5c8787 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 28 May 2009 22:42:30 -0700 Subject: [PATCH 06/12] Added helper for accessing the top level document. And more rails compatibility. --- lib/couchrest/more/casted_model.rb | 7 ++++ lib/couchrest/more/extended_document.rb | 13 ++++++ lib/couchrest/support/rails.rb | 9 +++++ spec/couchrest/more/casted_model_spec.rb | 50 ++++++++++++++++++++++++ spec/fixtures/more/person.rb | 1 + 5 files changed, 80 insertions(+) diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index 2811de3..6740263 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -27,6 +27,13 @@ module CouchRest super(key.to_s) end + # Gets a reference to the top level extended + # document that a model is saved inside of + def base_doc + raise "Cannot call base_doc on a model that is not yet casted by a document" unless @casted_by + @casted_by.base_doc + end + # False if the casted model has already # been saved in the containing document def new_model? diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 273e204..dbc929c 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -110,6 +110,19 @@ module CouchRest self.class.properties end + # Gets a reference to the actual document in the DB + # Calls up to the next document if there is one, + # Otherwise we're at the top and we return self + def base_doc + return self if base_doc? + @casted_by.base_doc + end + + # Checks if we're the top document + def base_doc? + !@casted_by + end + # Takes a hash as argument, and applies the values by using writer methods # 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. diff --git a/lib/couchrest/support/rails.rb b/lib/couchrest/support/rails.rb index 19374c0..eaf1650 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/support/rails.rb @@ -21,8 +21,17 @@ CouchRest::Document.class_eval do super end alias_method :kind_of?, :is_a? + alias_method :to_param, :id 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 + base_doc.id + end + alias_method :to_param, :id +end require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors') diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index b493656..faf175d 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -5,6 +5,7 @@ require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'question') +require File.join(FIXTURE_PATH, 'more', 'course') class WithCastedModelMixin < Hash @@ -299,4 +300,53 @@ describe CouchRest::CastedModel do @cat.toys.first.new_model?.should be_true end end + + describe "calling base_doc from a nested casted model" do + before :each do + @course = Course.new(:title => 'Science 101') + @professor = Person.new(:name => 'Professor Plum') + @cat = Cat.new(:name => 'Scratchy') + @toy1 = CatToy.new + @toy2 = CatToy.new + @course.professor = @professor + @professor.pet = @cat + @cat.favorite_toy = @toy1 + @cat.toys << @toy2 + end + + it "should reference the top document for" do + @course.base_doc.should === @course + @professor.base_doc.should === @course + @cat.base_doc.should === @course + @toy1.base_doc.should === @course + @toy2.base_doc.should === @course + end + + it "should call setter on top document" do + @toy1.base_doc.title = 'Tom Foolery' + @course.title.should == 'Tom Foolery' + end + end + + describe "calling base_doc.save from a nested casted model" do + before :each do + reset_test_db! + @cat = Cat.new(:name => 'Snowball') + @toy = CatToy.new + @cat.favorite_toy = @toy + end + + it "should not save parent document when casted model is invalid" do + @toy.should_not be_valid + @toy.base_doc.save.should be_false + lambda{@toy.base_doc.save!}.should raise_error + end + + it "should save parent document when nested casted model is valid" do + @toy.name = "Mr Squeaks" + @toy.should be_valid + @toy.base_doc.save.should be_true + lambda{@toy.base_doc.save!}.should_not raise_error + end + end end diff --git a/spec/fixtures/more/person.rb b/spec/fixtures/more/person.rb index ddc1bfd..de9e72c 100644 --- a/spec/fixtures/more/person.rb +++ b/spec/fixtures/more/person.rb @@ -1,6 +1,7 @@ class Person < Hash include ::CouchRest::CastedModel property :name + property :pet, :cast_as => 'Cat' def last_name name.last From fb3c4530edd69edd1532c558c093aae9bb0f8f58 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sat, 30 May 2009 14:53:55 -0700 Subject: [PATCH 07/12] Fixed a failing spec when using ruby 1.9 --- spec/couchrest/core/database_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 57dc2a4..6a548a9 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -258,7 +258,9 @@ describe CouchRest::Database do @file.close end it "should save the attachment to a new doc" do - r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image = @file.read, {:content_type => 'image/png'}) + image = @file.read + image.force_encoding('ASCII-8BIT') if image.respond_to?(:force_encoding) + r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image, {:content_type => 'image/png'}) r['ok'].should == true doc = @db.get("attach-this") attachment = @db.fetch_attachment(doc,"couchdb.png") From 027dd9a3ee1eb67499bf7a6643109ae283999e37 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sat, 30 May 2009 15:47:04 -0700 Subject: [PATCH 08/12] A better fix for failing spec --- spec/couchrest/core/database_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 6a548a9..b04c278 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -252,15 +252,13 @@ describe CouchRest::Database do describe "PUT attachment from file" do before(:each) do filename = FIXTURE_PATH + '/attachments/couchdb.png' - @file = File.open(filename) + @file = File.open(filename, "rb") end after(:each) do @file.close end it "should save the attachment to a new doc" do - image = @file.read - image.force_encoding('ASCII-8BIT') if image.respond_to?(:force_encoding) - r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image, {:content_type => 'image/png'}) + r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image = @file.read, {:content_type => 'image/png'}) r['ok'].should == true doc = @db.get("attach-this") attachment = @db.fetch_attachment(doc,"couchdb.png") From 91cd1d9c7b7b8a14e517d1045c918a35185e0e89 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sat, 30 May 2009 23:20:39 -0700 Subject: [PATCH 09/12] base_doc should be nil for unassociated casted models --- lib/couchrest/more/casted_model.rb | 2 +- lib/couchrest/support/rails.rb | 1 + spec/couchrest/more/casted_model_spec.rb | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index 6740263..e2c5522 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -30,7 +30,7 @@ module CouchRest # Gets a reference to the top level extended # document that a model is saved inside of def base_doc - raise "Cannot call base_doc on a model that is not yet casted by a document" unless @casted_by + return nil unless @casted_by @casted_by.base_doc end diff --git a/lib/couchrest/support/rails.rb b/lib/couchrest/support/rails.rb index eaf1650..37b4c23 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/support/rails.rb @@ -28,6 +28,7 @@ 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 diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index faf175d..9a22fee 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -326,6 +326,11 @@ describe CouchRest::CastedModel do @toy1.base_doc.title = 'Tom Foolery' @course.title.should == 'Tom Foolery' end + + it "should return nil if not yet casted" do + person = Person.new + person.base_doc.should == nil + end end describe "calling base_doc.save from a nested casted model" do From b4e2250668f6124fdd0040a921e1524bbf1324c5 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 4 Jun 2009 19:49:10 -0700 Subject: [PATCH 10/12] Added validation callbacks to extended documents and casted models --- README.md | 9 ++++-- lib/couchrest/mixins/validation.rb | 8 ++++- lib/couchrest/more/casted_model.rb | 1 + spec/couchrest/more/casted_model_spec.rb | 41 ++++++++++++++++++++++++ spec/couchrest/more/extended_doc_spec.rb | 22 +++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e6f9bf..b9ed345 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,13 @@ CouchRest::Model has been deprecated and replaced by CouchRest::ExtendedDocument ### Callbacks -`CouchRest::ExtendedDocuments` instances have 2 callbacks already defined for you: - `create_callback`, `save_callback`, `update_callback` and `destroy_callback` +`CouchRest::ExtendedDocuments` instances have 4 callbacks already defined for you: + `validate_callback`, `create_callback`, `save_callback`, `update_callback` and `destroy_callback` -In your document inherits from `CouchRest::ExtendedDocument`, define your callback as follows: +`CouchRest::CastedModel` instances have 1 callback already defined for you: + `validate_callback` + +Define your callback as follows: save_callback :before, :generate_slug_from_name diff --git a/lib/couchrest/mixins/validation.rb b/lib/couchrest/mixins/validation.rb index dda708e..467e143 100644 --- a/lib/couchrest/mixins/validation.rb +++ b/lib/couchrest/mixins/validation.rb @@ -51,6 +51,9 @@ module CouchRest def self.included(base) base.extlib_inheritable_accessor(:auto_validation) base.class_eval <<-EOS, __FILE__, __LINE__ + # Callbacks + define_callbacks :validate + # Turn off auto validation by default self.auto_validation ||= false @@ -72,6 +75,7 @@ module CouchRest base.extend(ClassMethods) base.class_eval <<-EOS, __FILE__, __LINE__ + define_callbacks :validate if method_defined?(:_run_save_callbacks) save_callback :before, :check_validations end @@ -147,7 +151,9 @@ module CouchRest valid = recursive_valid?(prop, context, valid) && valid end end - target.class.validators.execute(context, target) && valid + target._run_validate_callbacks do + target.class.validators.execute(context, target) && valid + end end diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index e2c5522..25e9031 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -4,6 +4,7 @@ module CouchRest module CastedModel def self.included(base) + base.send(:include, CouchRest::Callbacks) base.send(:include, CouchRest::Mixins::Properties) base.send(:attr_accessor, :casted_by) base.send(:attr_accessor, :document_saved) diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 9a22fee..678c81f 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -23,6 +23,26 @@ class DummyModel < CouchRest::ExtendedDocument property :keywords, :cast_as => ["String"] end +class CastedCallbackDoc < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + raise "Default DB not set" if TEST_SERVER.default_database.nil? + property :callback_model, :cast_as => 'WithCastedCallBackModel' +end +class WithCastedCallBackModel < Hash + include CouchRest::CastedModel + include CouchRest::Validation + property :name + property :run_before_validate + property :run_after_validate + + validate_callback :before do |object| + object.run_before_validate = true + end + validate_callback :after do |object| + object.run_after_validate = true + end +end + describe CouchRest::CastedModel do describe "A non hash class including CastedModel" do @@ -354,4 +374,25 @@ describe CouchRest::CastedModel do lambda{@toy.base_doc.save!}.should_not raise_error end end + + describe "callbacks" do + before(:each) do + @doc = CastedCallbackDoc.new + @model = WithCastedCallBackModel.new + @doc.callback_model = @model + end + + describe "validate" do + it "should run before_validate before validating" do + @model.run_before_validate.should be_nil + @model.should be_valid + @model.run_before_validate.should be_true + end + it "should run after_validate after validating" do + @model.run_after_validate.should be_nil + @model.should be_valid + @model.run_after_validate.should be_true + end + end + end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index dcec051..dd02a91 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -17,8 +17,11 @@ describe "ExtendedDocument" do end class WithCallBacks < CouchRest::ExtendedDocument + include ::CouchRest::Validation use_database TEST_SERVER.default_database property :name + property :run_before_validate + property :run_after_validate property :run_before_save property :run_after_save property :run_before_create @@ -26,6 +29,12 @@ describe "ExtendedDocument" do property :run_before_update property :run_after_update + validate_callback :before do |object| + object.run_before_validate = true + end + validate_callback :after do |object| + object.run_after_validate = true + end save_callback :before do |object| object.run_before_save = true end @@ -504,6 +513,19 @@ describe "ExtendedDocument" do @doc = WithCallBacks.new end + + describe "validate" do + it "should run before_validate before validating" do + @doc.run_before_validate.should be_nil + @doc.should be_valid + @doc.run_before_validate.should be_true + end + it "should run after_validate after validating" do + @doc.run_after_validate.should be_nil + @doc.should be_valid + @doc.run_after_validate.should be_true + end + end describe "save" do it "should run the after filter after saving" do @doc.run_after_save.should be_nil From 76b1563539c258cf17d60c1b93a0c3d26be1075a Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Thu, 4 Jun 2009 20:44:44 -0700 Subject: [PATCH 11/12] Renamed new_document? and new_model? to simply new? --- lib/couchrest/core/document.rb | 4 ++-- lib/couchrest/mixins/properties.rb | 2 +- lib/couchrest/more/casted_model.rb | 4 ++-- lib/couchrest/more/extended_document.rb | 12 ++++++------ spec/couchrest/more/casted_model_spec.rb | 20 ++++++++++---------- spec/couchrest/more/extended_doc_spec.rb | 6 +++--- spec/couchrest/more/property_spec.rb | 2 +- spec/fixtures/more/article.rb | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index e90c5fe..93b0411 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -27,7 +27,7 @@ module CouchRest end # returns true if the document has never been saved - def new_document? + def new? !rev end @@ -67,7 +67,7 @@ module CouchRest # Returns the CouchDB uri for the document def uri(append_rev = false) - return nil if new_document? + return nil if new? couch_uri = "http://#{database.uri}/#{CGI.escape(id)}" if append_rev == true couch_uri << "?rev=#{rev}" diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index ae18323..953e125 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -17,7 +17,7 @@ module CouchRest end def apply_defaults - return if self.respond_to?(:new_document?) && (new_document? == false) + return if self.respond_to?(:new?) && (new? == false) return unless self.class.respond_to?(:properties) return if self.class.properties.empty? # TODO: cache the default object diff --git a/lib/couchrest/more/casted_model.rb b/lib/couchrest/more/casted_model.rb index 25e9031..029dc14 100644 --- a/lib/couchrest/more/casted_model.rb +++ b/lib/couchrest/more/casted_model.rb @@ -37,10 +37,10 @@ module CouchRest # False if the casted model has already # been saved in the containing document - def new_model? + def new? !@document_saved end - alias :new_record? :new_model? + alias :new_record? :new? # Sets the attributes from a hash def update_attributes_without_saving(hash) diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index dbc929c..44c73c3 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -64,7 +64,7 @@ module CouchRest save_callback :before do |object| object['updated_at'] = Time.now - object['created_at'] = object['updated_at'] if object.new_document? + object['created_at'] = object['updated_at'] if object.new? end EOS end @@ -145,7 +145,7 @@ module CouchRest end # for compatibility with old-school frameworks - alias :new_record? :new_document? + alias :new_record? :new? # Trigger the callbacks (before, after, around) # and create the document @@ -166,7 +166,7 @@ module CouchRest # unlike save, create returns the newly created document def create_without_callbacks(bulk =false) raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database - set_unique_id if new_document? && self.respond_to?(:set_unique_id) + set_unique_id if new? && self.respond_to?(:set_unique_id) result = database.save_doc(self, bulk) (result["ok"] == true) ? self : false end @@ -181,7 +181,7 @@ module CouchRest # only if the document isn't new def update(bulk = false) caught = catch(:halt) do - if self.new_document? + if self.new? save(bulk) else _run_update_callbacks do @@ -197,7 +197,7 @@ module CouchRest # and save the document def save(bulk = false) caught = catch(:halt) do - if self.new_document? + if self.new? _run_save_callbacks do save_without_callbacks(bulk) end @@ -211,7 +211,7 @@ module CouchRest # Returns a boolean value def save_without_callbacks(bulk = false) raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database - set_unique_id if new_document? && self.respond_to?(:set_unique_id) + set_unique_id if new? && self.respond_to?(:set_unique_id) result = database.save_doc(self, bulk) mark_as_saved if result["ok"] == true result["ok"] == true diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 678c81f..029e294 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -280,7 +280,7 @@ describe CouchRest::CastedModel do end end - describe "calling new_model? on a casted model" do + describe "calling new? on a casted model" do before :each do reset_test_db! @cat = Cat.new(:name => 'Sockington') @@ -289,35 +289,35 @@ describe CouchRest::CastedModel do end it "should be true on new" do - CatToy.new.new_model?.should be_true + CatToy.new.should be_new CatToy.new.new_record?.should be_true end it "should be true after assignment" do - @cat.favorite_toy.new_model?.should be_true - @cat.toys.first.new_model?.should be_true + @cat.favorite_toy.should be_new + @cat.toys.first.should be_new end it "should not be true after create or save" do @cat.create @cat.save - @cat.favorite_toy.new_model?.should be_false - @cat.toys.first.new_model?.should be_false + @cat.favorite_toy.should_not be_new + @cat.toys.first.should_not be_new end it "should not be true after get from the database" do @cat.save @cat = Cat.get(@cat.id) - @cat.favorite_toy.new_model?.should be_false - @cat.toys.first.new_model?.should be_false + @cat.favorite_toy.should_not be_new + @cat.toys.first.should_not be_new end it "should still be true after a failed create or save" do @cat.name = nil @cat.create.should be_false @cat.save.should be_false - @cat.favorite_toy.new_model?.should be_true - @cat.toys.first.new_model?.should be_true + @cat.favorite_toy.should be_new + @cat.toys.first.should be_new end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index dd02a91..d4100bf 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -97,12 +97,12 @@ describe "ExtendedDocument" do it "should be a new_record" do @obj = Basic.new @obj.rev.should be_nil - @obj.should be_a_new_record + @obj.should be_new end it "should be a new_document" do @obj = Basic.new @obj.rev.should be_nil - @obj.should be_a_new_document + @obj.should be_new end end @@ -405,7 +405,7 @@ describe "ExtendedDocument" do end it "should be a new document" do - @art.should be_a_new_document + @art.should be_new @art.title.should be_nil end diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 57af71a..4f85fde 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -85,7 +85,7 @@ describe "ExtendedDocument properties" do @invoice.location = nil @invoice.should_not be_valid @invoice.save.should be_false - @invoice.should be_new_document + @invoice.should be_new end end diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index e0a6393..840b45b 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -29,6 +29,6 @@ class Article < CouchRest::ExtendedDocument save_callback :before, :generate_slug_from_title def generate_slug_from_title - self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document? + self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new? end end \ No newline at end of file From 1c6e073b47cdf09f47103d18fe4ba3f8dc2332a4 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sun, 7 Jun 2009 02:51:50 -0700 Subject: [PATCH 12/12] Added logger to rails support --- lib/couchrest/support/rails.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/couchrest/support/rails.rb b/lib/couchrest/support/rails.rb index 37b4c23..2fa267c 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/support/rails.rb @@ -14,6 +14,9 @@ end 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) @@ -21,7 +24,11 @@ CouchRest::Document.class_eval do super end alias_method :kind_of?, :is_a? - alias_method :to_param, :id + + # Gives extended doc a seamless logger + def logger + ActiveRecord::Base.logger + end end CouchRest::CastedModel.class_eval do