diff --git a/history.md b/history.md index d780dd0..24d0bd3 100644 --- a/history.md +++ b/history.md @@ -2,13 +2,18 @@ ## 1.1.0 - 2011-05-XX +* New Features + * Properties with a nil value are now no longer sent to the database. + * Now possible to build new objects via CastedArray#build + * Minor fixes * #as_json now correctly uses ActiveSupports methods. - * nil properties are now no longer sent in the document body. * Rails 3.1 support (Peter Williams) * Initialization blocks when creating new models (Peter Williams) * Removed railties dependency (DAddYE) * DesignDoc cache refreshed if a database is deleted. + * Fixing dirty tracking on collection_of association. + * Uniqueness Validation views created on initialization, not on demand! ## 1.1.0.beta5 - 2011-04-30 diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index ed2c23c..f8e5a12 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -153,7 +153,7 @@ module CouchRest def #{attrib}(reload = false) return @#{attrib} unless @#{attrib}.nil? or reload ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)} - @#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}') + @#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self) end EOS end @@ -161,7 +161,7 @@ module CouchRest def create_collection_of_setter(attrib, options) class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib}=(value) - @#{attrib} = ::CouchRest::CollectionOfProxy.new(value, self, '#{options[:foreign_key]}') + @#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self) end EOS end @@ -169,67 +169,63 @@ module CouchRest end end - end - # Special proxy for a collection of items so that adding and removing - # to the list automatically updates the associated property. - class CollectionOfProxy < Array - attr_accessor :property - attr_accessor :casted_by + # Special proxy for a collection of items so that adding and removing + # to the list automatically updates the associated property. + class CollectionOfProxy < CastedArray - def initialize(array, casted_by, property) - self.property = property - self.casted_by = casted_by - (array ||= []).compact! - casted_by[property.to_s] = [] # replace the original array! - array.compact.each do |obj| - check_obj(obj) - casted_by[property.to_s] << obj.id + def initialize(array, property, parent) + (array ||= []).compact! + super(array, property, parent) + casted_by[casted_by_property.to_s] = [] # replace the original array! + array.compact.each do |obj| + check_obj(obj) + casted_by[casted_by_property.to_s] << obj.id + end + end + + def << obj + check_obj(obj) + casted_by[casted_by_property.to_s] << obj.id + super(obj) + end + + def push(obj) + check_obj(obj) + casted_by[casted_by_property.to_s].push obj.id + super(obj) + end + + def unshift(obj) + check_obj(obj) + casted_by[casted_by_property.to_s].unshift obj.id + super(obj) end - super(array) - end - - def << obj - check_obj(obj) - casted_by[property.to_s] << obj.id - super(obj) - end - - def push(obj) - check_obj(obj) - casted_by[property.to_s].push obj.id - super(obj) - end - - def unshift(obj) - check_obj(obj) - casted_by[property.to_s].unshift obj.id - super(obj) - end - def []= index, obj - check_obj(obj) - casted_by[property.to_s][index] = obj.id - super(index, obj) - end + def []= index, obj + check_obj(obj) + casted_by[casted_by_property.to_s][index] = obj.id + super(index, obj) + end - def pop - casted_by[property.to_s].pop - super - end - - def shift - casted_by[property.to_s].shift - super - end + def pop + casted_by[casted_by_property.to_s].pop + super + end + + def shift + casted_by[casted_by_property.to_s].shift + super + end - protected + protected + + def check_obj(obj) + raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new? + end - def check_obj(obj) - raise "Object cannot be added to #{casted_by.class.to_s}##{property.to_s} collection unless saved" if obj.new? end end - end diff --git a/lib/couchrest/model/casted_array.rb b/lib/couchrest/model/casted_array.rb index 6737b46..8e4ff66 100644 --- a/lib/couchrest/model/casted_array.rb +++ b/lib/couchrest/model/casted_array.rb @@ -50,6 +50,12 @@ module CouchRest::Model super end + def build(*args) + obj = casted_by_property.build(*args) + self.push(obj) + obj + end + protected def instantiate_and_cast(obj, change = true) diff --git a/lib/couchrest/model/property.rb b/lib/couchrest/model/property.rb index baa71ce..7661858 100644 --- a/lib/couchrest/model/property.rb +++ b/lib/couchrest/model/property.rb @@ -64,6 +64,18 @@ module CouchRest::Model end end + # Initialize a new instance of a property's type ready to be + # used. If a proc is defined for the init method, it will be used instead of + # a normal call to the class. + def build(*args) + raise StandardError, "Cannot build property without a class" if @type_class.nil? + if @init_method.is_a?(Proc) + @init_method.call(*args) + else + @type_class.send(@init_method, *args) + end + end + private def associate_casted_value_to_parent(parent, value) diff --git a/lib/couchrest/model/typecast.rb b/lib/couchrest/model/typecast.rb index 6e1113d..6858335 100644 --- a/lib/couchrest/model/typecast.rb +++ b/lib/couchrest/model/typecast.rb @@ -14,8 +14,7 @@ module CouchRest elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass) send('typecast_to_'+klass.to_s.downcase, value) else - # Allow the init_method to be defined as a Proc for advanced conversion - property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value) + property.build(value) end end diff --git a/lib/couchrest/model/validations/uniqueness.rb b/lib/couchrest/model/validations/uniqueness.rb index c289849..6f9abdd 100644 --- a/lib/couchrest/model/validations/uniqueness.rb +++ b/lib/couchrest/model/validations/uniqueness.rb @@ -3,7 +3,7 @@ module CouchRest module Model module Validations - + # Validates if a field is unique class UniquenessValidator < ActiveModel::EachValidator @@ -11,29 +11,33 @@ module CouchRest # or add one if necessary. def setup(model) @model = model + if options[:view].blank? + attributes.each do |attribute| + opts = merge_view_options(attribute) + + if model.respond_to?(:has_view?) && !model.has_view?(opts[:view_name]) + opts[:keys] << {:allow_nil => true} + model.view_by(*opts[:keys]) + end + end + end end def validate_each(document, attribute, value) - keys = [attribute] - unless options[:scope].nil? - keys = (options[:scope].is_a?(Array) ? options[:scope] : [options[:scope]]) + keys - end - values = keys.map{|k| document.send(k)} - values = values.first if values.length == 1 + opts = merge_view_options(attribute) - view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view] + values = opts[:keys].map{|k| document.send(k)} + values = values.first if values.length == 1 model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model) # Determine the base of the search - base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy]) + base = opts[:proxy].nil? ? model : document.instance_eval(opts[:proxy]) - if base.respond_to?(:has_view?) && !base.has_view?(view_name) - raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil? - keys << {:allow_nil => true} - model.view_by(*keys) + if base.respond_to?(:has_view?) && !base.has_view?(opts[:view_name]) + raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!" end - rows = base.view(view_name, :key => values, :limit => 2, :include_docs => false)['rows'] + rows = base.view(opts[:view_name], :key => values, :limit => 2, :include_docs => false)['rows'] return if rows.empty? unless document.new? @@ -47,6 +51,17 @@ module CouchRest end end + private + + def merge_view_options(attr) + keys = [attr] + keys.unshift(*options[:scope]) unless options[:scope].nil? + + view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view] + + options.merge({:keys => keys, :view_name => view_name}) + end + end end diff --git a/spec/couchrest/assocations_spec.rb b/spec/couchrest/assocations_spec.rb index cb3b0c4..0affb6e 100644 --- a/spec/couchrest/assocations_spec.rb +++ b/spec/couchrest/assocations_spec.rb @@ -101,7 +101,7 @@ describe "Assocations" do it "should create an associated property and collection proxy" do @invoice.respond_to?('entry_ids').should be_true @invoice.respond_to?('entry_ids=').should be_true - @invoice.entries.class.should eql(::CouchRest::CollectionOfProxy) + @invoice.entries.class.should eql(::CouchRest::Model::CollectionOfProxy) end it "should allow replacement of objects" do @@ -154,6 +154,33 @@ describe "Assocations" do @invoice.entries.should be_empty end + # Account for dirty tracking + describe "dirty tracking" do + it "should register changes on push" do + @invoice.changed?.should be_false + @invoice.entries << @entries[0] + @invoice.changed?.should be_true + end + it "should register changes on pop" do + @invoice.entries << @entries[0] + @invoice.save + @invoice.changed?.should be_false + @invoice.entries.pop + @invoice.changed?.should be_true + end + it "should register id changes on push" do + @invoice.entry_ids << @entries[0].id + @invoice.changed?.should be_true + end + it "should register id changes on pop" do + @invoice.entry_ids << @entries[0].id + @invoice.save + @invoice.changed?.should be_false + @invoice.entry_ids.pop + @invoice.changed?.should be_true + end + end + describe "proxy" do it "should ensure new entries to proxy are matched" do diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index 08ea325..bb55bfc 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -358,6 +358,28 @@ describe "Property Class" do property.init_method.should eql('parse') end + describe "#build" do + it "should allow instantiation of new object" do + property = CouchRest::Model::Property.new(:test, Date) + obj = property.build(2011, 05, 21) + obj.should eql(Date.new(2011, 05, 21)) + end + it "should use init_method if provided" do + property = CouchRest::Model::Property.new(:test, Date, :init_method => 'parse') + obj = property.build("2011-05-21") + obj.should eql(Date.new(2011, 05, 21)) + end + it "should use init_method Proc if provided" do + property = CouchRest::Model::Property.new(:test, Date, :init_method => Proc.new{|v| Date.parse(v)}) + obj = property.build("2011-05-21") + obj.should eql(Date.new(2011, 05, 21)) + end + it "should raise error if no class" do + property = CouchRest::Model::Property.new(:test) + lambda { property.build }.should raise_error(StandardError, /Cannot build/) + end + end + ## Property Casting method. More thoroughly tested in typecast_spec. describe "casting" do @@ -386,6 +408,18 @@ describe "Property Class" do property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray) end + it "should allow instantion of model via CastedArray#build" do + property = CouchRest::Model::Property.new(:dates, [Date]) + parent = Article.new + ary = property.cast(parent, []) + obj = ary.build(2011, 05, 21) + ary.length.should eql(1) + ary.first.should eql(Date.new(2011, 05, 21)) + obj = ary.build(2011, 05, 22) + ary.length.should eql(2) + ary.last.should eql(Date.new(2011, 05, 22)) + end + it "should raise and error if value is array when type is not" do property = CouchRest::Model::Property.new(:test, Date) parent = mock("FooClass") diff --git a/spec/couchrest/validations_spec.rb b/spec/couchrest/validations_spec.rb index f8e2f4b..b387323 100644 --- a/spec/couchrest/validations_spec.rb +++ b/spec/couchrest/validations_spec.rb @@ -16,7 +16,11 @@ describe "Validations" do before(:all) do @objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)} end - + + it "should create a new view if none defined before performing" do + WithUniqueValidation.has_view?(:by_title).should be_true + end + it "should validate a new unique document" do @obj = WithUniqueValidation.create(:title => 'title 4') @obj.new?.should_not be_true @@ -35,6 +39,7 @@ describe "Validations" do @obj.should be_valid end + it "should allow own view to be specified" do # validates_uniqueness_of :code, :view => 'all' WithUniqueValidationView.create(:title => 'title 1', :code => '1234') @@ -50,6 +55,13 @@ describe "Validations" do }.should raise_error end + it "should not try to create a defined view" do + WithUniqueValidationView.validates_uniqueness_of :title, :view => 'fooobar' + WithUniqueValidationView.has_view?('fooobar').should be_false + WithUniqueValidationView.has_view?('by_title').should be_false + end + + it "should not try to create new view when already defined" do @obj = @objs[1] @obj.class.should_not_receive('view_by') @@ -60,6 +72,11 @@ describe "Validations" do end context "with a proxy parameter" do + + it "should create a new view despite proxy" do + WithUniqueValidationProxy.has_view?(:by_title).should be_true + end + it "should be used" do @obj = WithUniqueValidationProxy.new(:title => 'test 6') proxy = @obj.should_receive('proxy').and_return(@obj.class)