Merge branch 'master' of git://github.com/couchrest/couchrest_model

This commit is contained in:
Lucas Renan 2011-05-23 08:48:10 -03:00
commit ccafde77ab
9 changed files with 184 additions and 73 deletions

View file

@ -2,13 +2,18 @@
## 1.1.0 - 2011-05-XX ## 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 * Minor fixes
* #as_json now correctly uses ActiveSupports methods. * #as_json now correctly uses ActiveSupports methods.
* nil properties are now no longer sent in the document body.
* Rails 3.1 support (Peter Williams) * Rails 3.1 support (Peter Williams)
* Initialization blocks when creating new models (Peter Williams) * Initialization blocks when creating new models (Peter Williams)
* Removed railties dependency (DAddYE) * Removed railties dependency (DAddYE)
* DesignDoc cache refreshed if a database is deleted. * 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 ## 1.1.0.beta5 - 2011-04-30

View file

@ -153,7 +153,7 @@ module CouchRest
def #{attrib}(reload = false) def #{attrib}(reload = false)
return @#{attrib} unless @#{attrib}.nil? or reload return @#{attrib} unless @#{attrib}.nil? or reload
ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)} 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 end
EOS EOS
end end
@ -161,7 +161,7 @@ module CouchRest
def create_collection_of_setter(attrib, options) def create_collection_of_setter(attrib, options)
class_eval <<-EOS, __FILE__, __LINE__ + 1 class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{attrib}=(value) 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 end
EOS EOS
end end
@ -169,67 +169,63 @@ module CouchRest
end end
end end
end
# Special proxy for a collection of items so that adding and removing # Special proxy for a collection of items so that adding and removing
# to the list automatically updates the associated property. # to the list automatically updates the associated property.
class CollectionOfProxy < Array class CollectionOfProxy < CastedArray
attr_accessor :property
attr_accessor :casted_by
def initialize(array, casted_by, property) def initialize(array, property, parent)
self.property = property (array ||= []).compact!
self.casted_by = casted_by super(array, property, parent)
(array ||= []).compact! casted_by[casted_by_property.to_s] = [] # replace the original array!
casted_by[property.to_s] = [] # replace the original array! array.compact.each do |obj|
array.compact.each do |obj| check_obj(obj)
check_obj(obj) casted_by[casted_by_property.to_s] << obj.id
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 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 def []= index, obj
check_obj(obj) check_obj(obj)
casted_by[property.to_s][index] = obj.id casted_by[casted_by_property.to_s][index] = obj.id
super(index, obj) super(index, obj)
end end
def pop def pop
casted_by[property.to_s].pop casted_by[casted_by_property.to_s].pop
super super
end end
def shift def shift
casted_by[property.to_s].shift casted_by[casted_by_property.to_s].shift
super super
end 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 end
end end

View file

@ -50,6 +50,12 @@ module CouchRest::Model
super super
end end
def build(*args)
obj = casted_by_property.build(*args)
self.push(obj)
obj
end
protected protected
def instantiate_and_cast(obj, change = true) def instantiate_and_cast(obj, change = true)

View file

@ -64,6 +64,18 @@ module CouchRest::Model
end end
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 private
def associate_casted_value_to_parent(parent, value) def associate_casted_value_to_parent(parent, value)

View file

@ -14,8 +14,7 @@ module CouchRest
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass) elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
send('typecast_to_'+klass.to_s.downcase, value) send('typecast_to_'+klass.to_s.downcase, value)
else else
# Allow the init_method to be defined as a Proc for advanced conversion property.build(value)
property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
end end
end end

View file

@ -3,7 +3,7 @@
module CouchRest module CouchRest
module Model module Model
module Validations module Validations
# Validates if a field is unique # Validates if a field is unique
class UniquenessValidator < ActiveModel::EachValidator class UniquenessValidator < ActiveModel::EachValidator
@ -11,29 +11,33 @@ module CouchRest
# or add one if necessary. # or add one if necessary.
def setup(model) def setup(model)
@model = 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 end
def validate_each(document, attribute, value) def validate_each(document, attribute, value)
keys = [attribute] opts = merge_view_options(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
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) model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model)
# Determine the base of the search # 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) if base.respond_to?(:has_view?) && !base.has_view?(opts[:view_name])
raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil? raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!"
keys << {:allow_nil => true}
model.view_by(*keys)
end 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? return if rows.empty?
unless document.new? unless document.new?
@ -47,6 +51,17 @@ module CouchRest
end end
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
end end

View file

@ -101,7 +101,7 @@ describe "Assocations" do
it "should create an associated property and collection proxy" 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.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 end
it "should allow replacement of objects" do it "should allow replacement of objects" do
@ -154,6 +154,33 @@ describe "Assocations" do
@invoice.entries.should be_empty @invoice.entries.should be_empty
end 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 describe "proxy" do
it "should ensure new entries to proxy are matched" do it "should ensure new entries to proxy are matched" do

View file

@ -358,6 +358,28 @@ describe "Property Class" do
property.init_method.should eql('parse') property.init_method.should eql('parse')
end 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. ## Property Casting method. More thoroughly tested in typecast_spec.
describe "casting" do 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) property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
end 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 it "should raise and error if value is array when type is not" do
property = CouchRest::Model::Property.new(:test, Date) property = CouchRest::Model::Property.new(:test, Date)
parent = mock("FooClass") parent = mock("FooClass")

View file

@ -16,7 +16,11 @@ describe "Validations" do
before(:all) do before(:all) do
@objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)} @objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)}
end 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 it "should validate a new unique document" do
@obj = WithUniqueValidation.create(:title => 'title 4') @obj = WithUniqueValidation.create(:title => 'title 4')
@obj.new?.should_not be_true @obj.new?.should_not be_true
@ -35,6 +39,7 @@ describe "Validations" do
@obj.should be_valid @obj.should be_valid
end end
it "should allow own view to be specified" do it "should allow own view to be specified" do
# validates_uniqueness_of :code, :view => 'all' # validates_uniqueness_of :code, :view => 'all'
WithUniqueValidationView.create(:title => 'title 1', :code => '1234') WithUniqueValidationView.create(:title => 'title 1', :code => '1234')
@ -50,6 +55,13 @@ describe "Validations" do
}.should raise_error }.should raise_error
end 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 it "should not try to create new view when already defined" do
@obj = @objs[1] @obj = @objs[1]
@obj.class.should_not_receive('view_by') @obj.class.should_not_receive('view_by')
@ -60,6 +72,11 @@ describe "Validations" do
end end
context "with a proxy parameter" do 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 it "should be used" do
@obj = WithUniqueValidationProxy.new(:title => 'test 6') @obj = WithUniqueValidationProxy.new(:title => 'test 6')
proxy = @obj.should_receive('proxy').and_return(@obj.class) proxy = @obj.should_receive('proxy').and_return(@obj.class)