diff --git a/VERSION b/VERSION index 2304d76..0859045 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0.beta5 +1.1.0.rc diff --git a/couchrest_model.gemspec b/couchrest_model.gemspec index ce7f2ab..7a500b7 100644 --- a/couchrest_model.gemspec +++ b/couchrest_model.gemspec @@ -23,11 +23,12 @@ Gem::Specification.new do |s| s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] - s.add_dependency(%q, "1.1.0.pre2") + s.add_dependency(%q, "1.1.0.pre3") s.add_dependency(%q, "~> 1.15") s.add_dependency(%q, "~> 3.0") s.add_dependency(%q, "~> 0.3.22") - s.add_development_dependency(%q, ">= 2.0.0") + s.add_development_dependency(%q, "~> 2.6.0") + s.add_development_dependency(%q, ["~> 1.5.1"]) s.add_development_dependency(%q, ">= 0.5.7") # s.add_development_dependency("jruby-openssl", ">= 0.7.3") end diff --git a/history.md b/history.md index b2df9f5..d8737a0 100644 --- a/history.md +++ b/history.md @@ -1,11 +1,11 @@ # CouchRest Model Change History -## 1.1.0 - 2011-05-XX +## 1.1.0.rc - 2011-06-08 * New Features * Properties with a nil value are now no longer sent to the database. * Now possible to build new objects via CastedArray#build - * Implement #get! and #find! class methods + * Implement #get! and #find! class methods * Minor fixes * #as_json now correctly uses ActiveSupports methods. @@ -18,6 +18,8 @@ * #destroy freezes object instead of removing _id and _rev, better for callbacks (pointer by karmi) * #destroyed? method now available * #reload no longer uses Hash#merge! which was causing issues with dirty tracking on casted models. (pointer by kostia) + * Non-property mass assignment on #new no longer possible without :directly_set_attributes option. + * Using CouchRest 1.1.0.pre3. (No more Hashes!) ## 1.1.0.beta5 - 2011-04-30 diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index 5f3a6cd..bf64e4b 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -1,6 +1,6 @@ module CouchRest module Model - class Base < Document + class Base < CouchRest::Document extend ActiveModel::Naming @@ -51,14 +51,15 @@ module CouchRest # # If a block is provided the new model will be passed into the # block so that it can be populated. - def initialize(doc = {}, options = {}) - doc = prepare_all_attributes(doc, options) - # set the instances database, if provided + def initialize(attributes = {}, options = {}) + super() + prepare_all_attributes(attributes, options) + # set the instance's database, if provided self.database = options[:database] unless options[:database].nil? - super(doc) unless self['_id'] && self['_rev'] self[self.model_type_key] = self.class.to_s end + yield self if block_given? after_initialize if respond_to?(:after_initialize) @@ -79,16 +80,6 @@ module CouchRest super end - ## Compatibility with ActiveSupport and older frameworks - - # Hack so that CouchRest::Document, which descends from Hash, - # doesn't appear to Rails routing as a Hash of options - def is_a?(klass) - return false if klass == Hash - super - end - alias :kind_of? :is_a? - def persisted? !new? end diff --git a/lib/couchrest/model/document_queries.rb b/lib/couchrest/model/document_queries.rb index 839674b..22aec40 100644 --- a/lib/couchrest/model/document_queries.rb +++ b/lib/couchrest/model/document_queries.rb @@ -1,13 +1,10 @@ module CouchRest module Model module DocumentQueries - - def self.included(base) - base.extend(ClassMethods) - end - + extend ActiveSupport::Concern + module ClassMethods - + # Load all documents that have the model_type_key's field equal to the # name of the current class. Take the standard set of # CouchRest::Database#view options. @@ -73,7 +70,7 @@ module CouchRest end end alias :find :get - + # Load a document from the database by id # An exception will be raised if the document isn't found # diff --git a/lib/couchrest/model/persistence.rb b/lib/couchrest/model/persistence.rb index c63b8f4..03a9ee8 100644 --- a/lib/couchrest/model/persistence.rb +++ b/lib/couchrest/model/persistence.rb @@ -106,14 +106,17 @@ module CouchRest module ClassMethods - # Creates a new instance, bypassing attribute protection + # Creates a new instance, bypassing attribute protection and + # uses the type field to determine which model to use to instanatiate + # the new object. # # ==== Returns # a document instance # - def build_from_database(doc = {}) - base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize - base.new(doc, :directly_set_attributes => true) + def build_from_database(doc = {}, options = {}, &block) + src = doc[model_type_key] + base = (src.blank? || src == self.to_s) ? self : src.constantize + base.new(doc, options.merge(:directly_set_attributes => true), &block) end # Defines an instance and save it directly to the database diff --git a/lib/couchrest/model/properties.rb b/lib/couchrest/model/properties.rb index 5d815ab..133e9b9 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -80,17 +80,18 @@ module CouchRest self.disable_dirty = dirty end - def prepare_all_attributes(doc = {}, options = {}) + def prepare_all_attributes(attrs = {}, options = {}) self.disable_dirty = !!options[:directly_set_attributes] apply_all_property_defaults if options[:directly_set_attributes] - directly_set_read_only_attributes(doc) + directly_set_read_only_attributes(attrs) + directly_set_attributes(attrs, true) else - doc = remove_protected_attributes(doc) + attrs = remove_protected_attributes(attrs) + directly_set_attributes(attrs) end - res = doc.nil? ? doc : directly_set_attributes(doc) self.disable_dirty = false - res + self end def find_property!(property) @@ -101,16 +102,13 @@ module CouchRest # Set all the attributes and return a hash with the attributes # that have not been accepted. - def directly_set_attributes(hash) - hash.reject do |attribute_name, attribute_value| - if self.respond_to?("#{attribute_name}=") - self.send("#{attribute_name}=", attribute_value) - true - elsif mass_assign_any_attribute # config option - self[attribute_name] = attribute_value - true - else - false + def directly_set_attributes(hash, mass_assign = false) + return if hash.nil? + hash.reject do |key, value| + if self.respond_to?("#{key}=") + self.send("#{key}=", value) + elsif mass_assign || mass_assign_any_attribute + self[key] = value end end end diff --git a/lib/couchrest/model/property.rb b/lib/couchrest/model/property.rb index 7661858..07fdfe8 100644 --- a/lib/couchrest/model/property.rb +++ b/lib/couchrest/model/property.rb @@ -26,7 +26,7 @@ module CouchRest::Model if type.is_a?(Array) if value.nil? value = [] - elsif [Hash, HashWithIndifferentAccess].include?(value.class) + elsif value.is_a?(Hash) # Assume provided as a Hash where key is index! data = value value = [ ] @@ -39,7 +39,7 @@ module CouchRest::Model arr = value.collect { |data| cast_value(parent, data) } # allow casted_by calls to be passed up chain by wrapping in CastedArray CastedArray.new(arr, self, parent) - elsif (type == Object || type == Hash) && (value.class == Hash) + elsif (type == Object || type == Hash) && (value.is_a?(Hash)) # allow casted_by calls to be passed up chain by wrapping in CastedHash CastedHash[value, self, parent] elsif !value.nil? diff --git a/lib/couchrest/model/proxyable.rb b/lib/couchrest/model/proxyable.rb index 53a88da..9fb812c 100644 --- a/lib/couchrest/model/proxyable.rb +++ b/lib/couchrest/model/proxyable.rb @@ -73,12 +73,12 @@ module CouchRest end # Base - def new(*args) - proxy_update(model.new(*args)) + def new(attrs = {}, options = {}, &block) + proxy_block_update(:new, attrs, options, &block) end - def build_from_database(doc = {}) - proxy_update(model.build_from_database(doc)) + def build_from_database(attrs = {}, options = {}, &block) + proxy_block_update(:build_from_database, attrs, options, &block) end def method_missing(m, *args, &block) @@ -170,6 +170,13 @@ module CouchRest end end + def proxy_block_update(method, *args, &block) + model.send(method, *args) do |doc| + proxy_update(doc) + yield doc if block_given? + end + end + end end end diff --git a/lib/couchrest/model/support/couchrest_design.rb b/lib/couchrest/model/support/couchrest_design.rb index a5caf07..73e9eaa 100644 --- a/lib/couchrest/model/support/couchrest_design.rb +++ b/lib/couchrest/model/support/couchrest_design.rb @@ -18,7 +18,7 @@ CouchRest::Design.class_eval do flatten = lambda {|r| (recurse = lambda {|v| - if v.is_a?(Hash) + if v.is_a?(Hash) || v.is_a?(CouchRest::Document) v.to_a.map{|v| recurse.call(v)}.flatten elsif v.is_a?(Array) v.flatten.map{|v| recurse.call(v)} diff --git a/spec/couchrest/base_spec.rb b/spec/couchrest/base_spec.rb index 208d6e7..e3c665f 100644 --- a/spec/couchrest/base_spec.rb +++ b/spec/couchrest/base_spec.rb @@ -49,8 +49,34 @@ describe "Model Base" do @obj.database.should eql('database') end + it "should only set defined properties" do + @doc = WithDefaultValues.new(:name => 'test', :foo => 'bar') + @doc['name'].should eql('test') + @doc['foo'].should be_nil + end + + it "should set all properties with :directly_set_attributes option" do + @doc = WithDefaultValues.new({:name => 'test', :foo => 'bar'}, :directly_set_attributes => true) + @doc['name'].should eql('test') + @doc['foo'].should eql('bar') + end + + it "should set the model type" do + @doc = WithDefaultValues.new() + @doc[WithDefaultValues.model_type_key].should eql('WithDefaultValues') + end + + it "should call after_initialize method if available" do + @doc = WithAfterInitializeMethod.new + @doc['some_value'].should eql('value') + end + + it "should call after_initialize after block" do + @doc = WithAfterInitializeMethod.new {|d| d.some_value = "foo"} + @doc['some_value'].should eql('foo') + end end - + describe "ActiveModel compatability Basic" do before(:each) do @@ -109,9 +135,23 @@ describe "Model Base" do end end + describe "#destroyed?" do + it "should be present" do + @obj.should respond_to(:destroyed?) + end + it "should return false with new object" do + @obj.destroyed?.should be_false + end + it "should return true after destroy" do + @obj.save + @obj.destroy + @obj.destroyed?.should be_true + end + end + end - + describe "update attributes without saving" do before(:each) do a = Article.get "big-bad-danger" rescue nil @@ -152,7 +192,7 @@ describe "Model Base" do }.should_not raise_error @art.slug.should == "big-bad-danger" end - + #it "should not change other attributes if there is an error" do # lambda { # @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") @@ -160,7 +200,7 @@ describe "Model Base" do # @art['title'].should == "big bad danger" #end end - + describe "update attributes" do before(:each) do a = Article.get "big-bad-danger" rescue nil @@ -175,7 +215,7 @@ describe "Model Base" do loaded['title'].should == "super danger" end end - + describe "with default" do it "should have the default value set at initalization" do @obj.preset.should == {:right => 10, :top_align => false} @@ -232,7 +272,7 @@ describe "Model Base" do WithTemplateAndUniqueID.all.map{|o| o.destroy} WithTemplateAndUniqueID.database.bulk_delete @tmpl = WithTemplateAndUniqueID.new - @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1') + @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'slug' => '1') end it "should have fields set when new" do @tmpl.preset.should == 'value' @@ -253,10 +293,10 @@ describe "Model Base" do before(:all) do WithTemplateAndUniqueID.all.map{|o| o.destroy} WithTemplateAndUniqueID.database.bulk_delete - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - WithTemplateAndUniqueID.new('important-field' => '4').save + WithTemplateAndUniqueID.new('slug' => '1').save + WithTemplateAndUniqueID.new('slug' => '2').save + WithTemplateAndUniqueID.new('slug' => '3').save + WithTemplateAndUniqueID.new('slug' => '4').save end it "should find all" do rs = WithTemplateAndUniqueID.all @@ -274,9 +314,9 @@ describe "Model Base" do end it ".count should return the number of documents" do - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save + WithTemplateAndUniqueID.new('slug' => '1').save + WithTemplateAndUniqueID.new('slug' => '2').save + WithTemplateAndUniqueID.new('slug' => '3').save WithTemplateAndUniqueID.count.should == 3 end @@ -285,14 +325,14 @@ describe "Model Base" do describe "finding the first instance of a model" do before(:each) do @db = reset_test_db! - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - WithTemplateAndUniqueID.new('important-field' => '4').save + WithTemplateAndUniqueID.new('slug' => '1').save + WithTemplateAndUniqueID.new('slug' => '2').save + WithTemplateAndUniqueID.new('slug' => '3').save + WithTemplateAndUniqueID.new('slug' => '4').save end it "should find first" do rs = WithTemplateAndUniqueID.first - rs['important-field'].should == "1" + rs['slug'].should == "1" end it "should return nil if no instances are found" do WithTemplateAndUniqueID.all.each {|obj| obj.destroy } @@ -370,14 +410,7 @@ describe "Model Base" do end end - describe "initialization" do - it "should call after_initialize method if available" do - @doc = WithAfterInitializeMethod.new - @doc['some_value'].should eql('value') - end - end - - describe "recursive validation on a model" do + describe "recursive validation on a model" do before :each do reset_test_db! @cat = Cat.new(:name => 'Sockington') diff --git a/spec/couchrest/design_doc_spec.rb b/spec/couchrest/design_doc_spec.rb index efda852..fcd40c1 100644 --- a/spec/couchrest/design_doc_spec.rb +++ b/spec/couchrest/design_doc_spec.rb @@ -202,7 +202,7 @@ describe "Design Documents" do describe "lazily refreshing the design document" do before(:all) do @db = reset_test_db! - WithTemplateAndUniqueID.new('important-field' => '1').save + WithTemplateAndUniqueID.new('slug' => '1').save end it "should not save the design doc twice" do WithTemplateAndUniqueID.all diff --git a/spec/couchrest/inherited_spec.rb b/spec/couchrest/inherited_spec.rb index 69eba6a..f19546f 100644 --- a/spec/couchrest/inherited_spec.rb +++ b/spec/couchrest/inherited_spec.rb @@ -1,40 +1,33 @@ require File.expand_path('../../spec_helper', __FILE__) -begin - require 'rubygems' unless ENV['SKIP_RUBYGEMS'] - require 'active_support/json' - ActiveSupport::JSON.backend = :JSONGem - - class PlainParent - class_inheritable_accessor :foo - self.foo = :bar - end - - class PlainChild < PlainParent - end - - class ExtendedParent < CouchRest::Model::Base - class_inheritable_accessor :foo - self.foo = :bar - end - - class ExtendedChild < ExtendedParent - end - - describe "Using chained inheritance without CouchRest::Model::Base" do - it "should preserve inheritable attributes" do - PlainParent.foo.should == :bar - PlainChild.foo.should == :bar - end - end - - describe "Using chained inheritance with CouchRest::Model::Base" do - it "should preserve inheritable attributes" do - ExtendedParent.foo.should == :bar - ExtendedChild.foo.should == :bar - end - end - -rescue LoadError - puts "This spec requires 'active_support/json' to be loaded" +class PlainParent + class_inheritable_accessor :foo + self.foo = :bar end + +class PlainChild < PlainParent +end + +class ExtendedParent < CouchRest::Model::Base + class_inheritable_accessor :foo + self.foo = :bar +end + +class ExtendedChild < ExtendedParent +end + +describe "Using chained inheritance without CouchRest::Model::Base" do + it "should preserve inheritable attributes" do + PlainParent.foo.should == :bar + PlainChild.foo.should == :bar + end +end + +describe "Using chained inheritance with CouchRest::Model::Base" do + it "should preserve inheritable attributes" do + ExtendedParent.foo.should == :bar + ExtendedChild.foo.should == :bar + end +end + + diff --git a/spec/couchrest/persistence_spec.rb b/spec/couchrest/persistence_spec.rb index 8def959..71ced97 100644 --- a/spec/couchrest/persistence_spec.rb +++ b/spec/couchrest/persistence_spec.rb @@ -35,11 +35,11 @@ describe "Model Persistence" do describe "basic saving and retrieving" do it "should work fine" do @obj.name = "should be easily saved and retrieved" - @obj.save - saved_obj = WithDefaultValues.get(@obj.id) + @obj.save! + saved_obj = WithDefaultValues.get!(@obj.id) saved_obj.should_not be_nil end - + it "should parse the Time attributes automatically" do @obj.name = "should parse the Time attributes automatically" @obj.set_by_proc.should be_an_instance_of(Time) @@ -223,34 +223,34 @@ describe "Model Persistence" do it "should require the field" do lambda{@templated.save}.should raise_error - @templated['important-field'] = 'very-important' + @templated['slug'] = 'very-important' @templated.save.should be_true end it "should save with the id" do - @templated['important-field'] = 'very-important' + @templated['slug'] = 'very-important' @templated.save.should be_true t = WithTemplateAndUniqueID.get('very-important') t.should == @templated end it "should not change the id on update" do - @templated['important-field'] = 'very-important' + @templated['slug'] = 'very-important' @templated.save.should be_true - @templated['important-field'] = 'not-important' + @templated['slug'] = 'not-important' @templated.save.should be_true t = WithTemplateAndUniqueID.get('very-important') t.id.should == @templated.id end it "should raise an error when the id is taken" do - @templated['important-field'] = 'very-important' + @templated['slug'] = 'very-important' @templated.save.should be_true - lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error + lambda{WithTemplateAndUniqueID.new('slug' => 'very-important').save}.should raise_error end it "should set the id" do - @templated['important-field'] = 'very-important' + @templated['slug'] = 'very-important' @templated.save.should be_true @templated.id.should == 'very-important' end diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index bb55bfc..fd82a14 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -315,6 +315,28 @@ describe "a casted model retrieved from the database" do end end +describe "nested models (not casted)" do + before(:each) do + reset_test_db! + @cat = ChildCat.new(:name => 'Stimpy') + @cat.mother = {:name => 'Stinky'} + @cat.siblings = [{:name => 'Feather'}, {:name => 'Felix'}] + @cat.save + @cat = ChildCat.get(@cat.id) + end + + it "should correctly save single relation" do + @cat.mother.name.should eql('Stinky') + @cat.mother.casted_by.should eql(@cat) + end + + it "should correctly save collection" do + @cat.siblings.first.name.should eql("Feather") + @cat.siblings.last.casted_by.should eql(@cat) + end + +end + describe "Property Class" do it "should provide name as string" do diff --git a/spec/couchrest/proxyable_spec.rb b/spec/couchrest/proxyable_spec.rb index 7e0c508..3dc2064 100644 --- a/spec/couchrest/proxyable_spec.rb +++ b/spec/couchrest/proxyable_spec.rb @@ -87,7 +87,7 @@ describe "Proxyable" do DummyProxyable.proxy_for(:cats) @obj = DummyProxyable.new CouchRest::Model::Proxyable::ModelProxy.should_receive(:new).with(Cat, @obj, 'dummy_proxyable', 'db').and_return(true) - @obj.should_receive('proxy_database').and_return('db') + @obj.should_receive(:proxy_database).and_return('db') @obj.cats end @@ -165,15 +165,13 @@ describe "Proxyable" do end it "should proxy new call" do - Cat.should_receive(:new).and_return({}) - @obj.should_receive(:proxy_update).and_return(true) - @obj.new + @obj.should_receive(:proxy_block_update).with(:new, 'attrs', 'opts') + @obj.new('attrs', 'opts') end it "should proxy build_from_database" do - Cat.should_receive(:build_from_database).and_return({}) - @obj.should_receive(:proxy_update).with({}).and_return(true) - @obj.build_from_database + @obj.should_receive(:proxy_block_update).with(:build_from_database, 'attrs', 'opts') + @obj.build_from_database('attrs', 'opts') end describe "#method_missing" do @@ -313,6 +311,15 @@ describe "Proxyable" do @obj.send(:proxy_update_all, docs) end + describe "#proxy_block_update" do + it "should proxy block updates" do + doc = { } + @obj.model.should_receive(:new).and_yield(doc) + @obj.should_receive(:proxy_update).with(doc) + @obj.send(:proxy_block_update, :new) + end + end + end end diff --git a/spec/fixtures/base.rb b/spec/fixtures/base.rb index d395b4a..65fabfa 100644 --- a/spec/fixtures/base.rb +++ b/spec/fixtures/base.rb @@ -86,15 +86,16 @@ end class WithTemplateAndUniqueID < CouchRest::Model::Base use_database TEST_SERVER.default_database unique_id do |model| - model['important-field'] + model.slug end + property :slug property :preset, :default => 'value' property :has_no_default end class WithGetterAndSetterMethods < CouchRest::Model::Base use_database TEST_SERVER.default_database - + property :other_arg def arg other_arg @@ -107,7 +108,7 @@ end class WithAfterInitializeMethod < CouchRest::Model::Base use_database TEST_SERVER.default_database - + property :some_value def after_initialize diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index 9e370e3..6e2af99 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -22,6 +22,7 @@ class Article < CouchRest::Model::Base property :date, Date property :slug, :read_only => true + property :user_id property :title property :tags, [String] diff --git a/spec/fixtures/more/cat.rb b/spec/fixtures/more/cat.rb index 903eafd..481bc37 100644 --- a/spec/fixtures/more/cat.rb +++ b/spec/fixtures/more/cat.rb @@ -17,3 +17,7 @@ class Cat < CouchRest::Model::Base property :number end +class ChildCat < Cat + property :mother, Cat + property :siblings, [Cat] +end