refactor #read_ and #write_attribute to behave the same when called with a missing property

This commit is contained in:
Will Leinweber 2010-08-11 22:27:53 -05:00
parent aac6b80d26
commit 1a7154f5bf
2 changed files with 90 additions and 71 deletions

View file

@ -2,9 +2,9 @@
module CouchRest module CouchRest
module Model module Model
module Properties module Properties
class IncludeError < StandardError; end class IncludeError < StandardError; end
def self.included(base) def self.included(base)
base.class_eval <<-EOS, __FILE__, __LINE__ + 1 base.class_eval <<-EOS, __FILE__, __LINE__ + 1
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
@ -23,12 +23,12 @@ module CouchRest
end end
def read_attribute(property) def read_attribute(property)
self[property.to_s] prop = find_property!(property)
self[prop.to_s]
end end
def write_attribute(property, value) def write_attribute(property, value)
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s} prop = find_property!(property)
raise "Missing property definition for #{property.to_s}" unless prop
self[prop.to_s] = prop.cast(self, value) self[prop.to_s] = prop.cast(self, value)
end end
@ -39,9 +39,16 @@ module CouchRest
write_attribute(property, property.default_value) write_attribute(property, property.default_value)
end end
end end
private
def find_property!(property)
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
raise ArgumentError, "Missing property definition for #{property.to_s}" unless prop
prop
end
module ClassMethods module ClassMethods
def property(name, *options, &block) def property(name, *options, &block)
opts = { } opts = { }
type = options.shift type = options.shift
@ -64,16 +71,16 @@ module CouchRest
class_eval <<-EOS, __FILE__, __LINE__ class_eval <<-EOS, __FILE__, __LINE__
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false) property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false) property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
set_callback :save, :before do |object| set_callback :save, :before do |object|
write_attribute('updated_at', Time.now) write_attribute('updated_at', Time.now)
write_attribute('created_at', Time.now) if object.new? write_attribute('created_at', Time.now) if object.new?
end end
EOS EOS
end end
protected protected
# This is not a thread safe operation, if you have to set new properties at runtime # This is not a thread safe operation, if you have to set new properties at runtime
# make sure a mutex is used. # make sure a mutex is used.
def define_property(name, options={}, &block) def define_property(name, options={}, &block)
@ -87,7 +94,7 @@ module CouchRest
type = [type] # inject as an array type = [type] # inject as an array
end end
property = Property.new(name, type, options) property = Property.new(name, type, options)
create_property_getter(property) create_property_getter(property)
create_property_setter(property) unless property.read_only == true create_property_setter(property) unless property.read_only == true
if property.type_class.respond_to?(:validates_casted_model) if property.type_class.respond_to?(:validates_casted_model)
validates_casted_model property.name validates_casted_model property.name
@ -95,7 +102,7 @@ module CouchRest
properties << property properties << property
property property
end end
# defines the getter for the property (and optional aliases) # defines the getter for the property (and optional aliases)
def create_property_getter(property) def create_property_getter(property)
# meth = property.name # meth = property.name
@ -136,9 +143,9 @@ module CouchRest
EOS EOS
end end
end end
end # module ClassMethods end # module ClassMethods
end end
end end
end end

View file

@ -11,36 +11,36 @@ require File.join(FIXTURE_PATH, 'more', 'course')
describe "Model properties" do describe "Model properties" do
before(:each) do before(:each) do
reset_test_db! reset_test_db!
@card = Card.new(:first_name => "matt") @card = Card.new(:first_name => "matt")
end end
it "should be accessible from the object" do it "should be accessible from the object" do
@card.properties.should be_an_instance_of(Array) @card.properties.should be_an_instance_of(Array)
@card.properties.map{|p| p.name}.should include("first_name") @card.properties.map{|p| p.name}.should include("first_name")
end end
it "should let you access a property value (getter)" do it "should let you access a property value (getter)" do
@card.first_name.should == "matt" @card.first_name.should == "matt"
end end
it "should let you set a property value (setter)" do it "should let you set a property value (setter)" do
@card.last_name = "Aimonetti" @card.last_name = "Aimonetti"
@card.last_name.should == "Aimonetti" @card.last_name.should == "Aimonetti"
end end
it "should not let you set a property value if it's read only" do it "should not let you set a property value if it's read only" do
lambda{@card.read_only_value = "test"}.should raise_error lambda{@card.read_only_value = "test"}.should raise_error
end end
it "should let you use an alias for an attribute" do it "should let you use an alias for an attribute" do
@card.last_name = "Aimonetti" @card.last_name = "Aimonetti"
@card.family_name.should == "Aimonetti" @card.family_name.should == "Aimonetti"
@card.family_name.should == @card.last_name @card.family_name.should == @card.last_name
end end
it "should let you use an alias for a casted attribute" do it "should let you use an alias for a casted attribute" do
@card.cast_alias = Person.new(:name => ["Aimonetti"]) @card.cast_alias = Person.new(:name => ["Aimonetti"])
@card.cast_alias.name.should == ["Aimonetti"] @card.cast_alias.name.should == ["Aimonetti"]
@ -50,7 +50,7 @@ describe "Model properties" do
card.calias.name.should == ["Aimonetti"] card.calias.name.should == ["Aimonetti"]
end end
it "should be auto timestamped" do it "should be auto timestamped" do
@card.created_at.should be_nil @card.created_at.should be_nil
@card.updated_at.should be_nil @card.updated_at.should be_nil
@ -59,45 +59,57 @@ describe "Model properties" do
@card.updated_at.should_not be_nil @card.updated_at.should_not be_nil
end end
it "should let you use read_attribute method" do describe '#read_attribute' do
@card.last_name = "Aimonetti" it "should let you use read_attribute method" do
@card.read_attribute(:last_name).should eql('Aimonetti') @card.last_name = "Aimonetti"
@card.read_attribute('last_name').should eql('Aimonetti') @card.read_attribute(:last_name).should eql('Aimonetti')
last_name_prop = @card.properties.find{|p| p.name == 'last_name'} @card.read_attribute('last_name').should eql('Aimonetti')
@card.read_attribute(last_name_prop).should eql('Aimonetti') last_name_prop = @card.properties.find{|p| p.name == 'last_name'}
@card.read_attribute(last_name_prop).should eql('Aimonetti')
end
it 'should raise an error if the property does not exist' do
expect { @card.read_attribute(:this_property_should_not_exist) }.to raise_error(ArgumentError)
end
end end
it "should let you use write_attribute method" do describe '#write_attribute' do
@card.write_attribute(:last_name, 'Aimonetti 1') it "should let you use write_attribute method" do
@card.last_name.should eql('Aimonetti 1') @card.write_attribute(:last_name, 'Aimonetti 1')
@card.write_attribute('last_name', 'Aimonetti 2') @card.last_name.should eql('Aimonetti 1')
@card.last_name.should eql('Aimonetti 2') @card.write_attribute('last_name', 'Aimonetti 2')
last_name_prop = @card.properties.find{|p| p.name == 'last_name'} @card.last_name.should eql('Aimonetti 2')
@card.write_attribute(last_name_prop, 'Aimonetti 3') last_name_prop = @card.properties.find{|p| p.name == 'last_name'}
@card.last_name.should eql('Aimonetti 3') @card.write_attribute(last_name_prop, 'Aimonetti 3')
@card.last_name.should eql('Aimonetti 3')
end
it 'should raise an error if the property does not exist' do
expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError)
end
it "should let you use write_attribute on readonly properties" do
lambda {
@card.read_only_value = "foo"
}.should raise_error
@card.write_attribute(:read_only_value, "foo")
@card.read_only_value.should == 'foo'
end
it "should cast via write_attribute" do
@card.write_attribute(:cast_alias, {:name => ["Sam", "Lown"]})
@card.cast_alias.class.should eql(Person)
@card.cast_alias.name.last.should eql("Lown")
end
it "should not cast via write_attribute if property not casted" do
@card.write_attribute(:first_name, {:name => "Sam"})
@card.first_name.class.should eql(Hash)
@card.first_name[:name].should eql("Sam")
end
end end
it "should let you use write_attribute on readonly properties" do
lambda {
@card.read_only_value = "foo"
}.should raise_error
@card.write_attribute(:read_only_value, "foo")
@card.read_only_value.should == 'foo'
end
it "should cast via write_attribute" do
@card.write_attribute(:cast_alias, {:name => ["Sam", "Lown"]})
@card.cast_alias.class.should eql(Person)
@card.cast_alias.name.last.should eql("Lown")
end
it "should not cast via write_attribute if property not casted" do
@card.write_attribute(:first_name, {:name => "Sam"})
@card.first_name.class.should eql(Hash)
@card.first_name[:name].should eql("Sam")
end
describe "mass assignment protection" do describe "mass assignment protection" do
it "should not store protected attribute using mass assignment" do it "should not store protected attribute using mass assignment" do
@ -120,12 +132,12 @@ describe "Model properties" do
end end
end end
describe "validation" do describe "validation" do
before(:each) do before(:each) do
@invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA") @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA")
end end
it "should be able to be validated" do it "should be able to be validated" do
@card.valid?.should == true @card.valid?.should == true
end end
@ -136,7 +148,7 @@ describe "Model properties" do
@card.errors.should_not be_empty @card.errors.should_not be_empty
@card.errors[:first_name].should == ["can't be blank"] @card.errors[:first_name].should == ["can't be blank"]
end end
it "should let you look up errors for a field by a string name" do it "should let you look up errors for a field by a string name" do
@card.first_name = nil @card.first_name = nil
@card.should_not be_valid @card.should_not be_valid
@ -150,13 +162,13 @@ describe "Model properties" do
@invoice.errors[:client_name].should == ["can't be blank"] @invoice.errors[:client_name].should == ["can't be blank"]
@invoice.errors[:employee_name].should_not be_empty @invoice.errors[:employee_name].should_not be_empty
end end
it "should let you set an error message" do it "should let you set an error message" do
@invoice.location = nil @invoice.location = nil
@invoice.valid? @invoice.valid?
@invoice.errors[:location].should == ["Hey stupid!, you forgot the location"] @invoice.errors[:location].should == ["Hey stupid!, you forgot the location"]
end end
it "should validate before saving" do it "should validate before saving" do
@invoice.location = nil @invoice.location = nil
@invoice.should_not be_valid @invoice.should_not be_valid
@ -164,7 +176,7 @@ describe "Model properties" do
@invoice.should be_new @invoice.should be_new
end end
end end
describe "casting" do describe "casting" do
before(:each) do before(:each) do
@course = Course.new(:title => 'Relaxation') @course = Course.new(:title => 'Relaxation')
@ -188,14 +200,14 @@ describe "Model properties" do
@course['started_on'].should be_an_instance_of(Date) @course['started_on'].should be_an_instance_of(Date)
end end
end end
describe "when type primitive is a String" do describe "when type primitive is a String" do
it "keeps string value unchanged" do it "keeps string value unchanged" do
value = "1.0" value = "1.0"
@course.title = value @course.title = value
@course['title'].should equal(value) @course['title'].should equal(value)
end end
it "it casts to string representation of the value" do it "it casts to string representation of the value" do
@course.title = 1.0 @course.title = 1.0
@course['title'].should eql("1.0") @course['title'].should eql("1.0")
@ -584,7 +596,7 @@ describe "Model properties" do
@course['klass'].should eql('NoClass') @course['klass'].should eql('NoClass')
end end
end end
describe 'when type primitive is a Boolean' do describe 'when type primitive is a Boolean' do
[ true, 'true', 'TRUE', '1', 1, 't', 'T' ].each do |value| [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].each do |value|
@ -609,7 +621,7 @@ describe "Model properties" do
end end
it "should respond to requests with ? modifier" do it "should respond to requests with ? modifier" do
@course.active = nil @course.active = nil
@course.active?.should be_false @course.active?.should be_false
@course.active = false @course.active = false
@course.active?.should be_false @course.active?.should be_false
@ -618,7 +630,7 @@ describe "Model properties" do
end end
it "should respond to requests with ? modifier on TrueClass" do it "should respond to requests with ? modifier on TrueClass" do
@course.very_active = nil @course.very_active = nil
@course.very_active?.should be_false @course.very_active?.should be_false
@course.very_active = false @course.very_active = false
@course.very_active?.should be_false @course.very_active?.should be_false
@ -631,7 +643,7 @@ describe "Model properties" do
end end
describe "properties of array of casted models" do describe "properties of array of casted models" do
before(:each) do before(:each) do
@course = Course.new :title => 'Test Course' @course = Course.new :title => 'Test Course'
end end
@ -691,13 +703,13 @@ describe "a casted model retrieved from the database" do
@cat.save @cat.save
@cat = Cat.get(@cat.id) @cat = Cat.get(@cat.id)
end end
describe "as a casted property" do describe "as a casted property" do
it "should already be casted_by its parent" do it "should already be casted_by its parent" do
@cat.favorite_toy.casted_by.should === @cat @cat.favorite_toy.casted_by.should === @cat
end end
end end
describe "from a casted collection" do describe "from a casted collection" do
it "should already be casted_by its parent" do it "should already be casted_by its parent" do
@cat.toys[0].casted_by.should === @cat @cat.toys[0].casted_by.should === @cat
@ -789,7 +801,7 @@ describe "Property Class" do
it "should set parent as casted_by object in CastedArray" do it "should set parent as casted_by object in CastedArray" do
property = CouchRest::Model::Property.new(:test, [Object]) property = CouchRest::Model::Property.new(:test, [Object])
parent = mock("FooObject") parent = mock("FooObject")
property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by.should eql(parent) property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by.should eql(parent)
end end
it "should set casted_by on new value" do it "should set casted_by on new value" do