Adds suppport for ActiveModel::Dirty and ::AttributeMethods

* ActiveModel::Dirty
** Basic support for dirty tracking
** It does not bubble up any changes to casted models currently

* ActiveModel::AttributeMethods
** Attributes are now read and written through ActiveModel
** This also allows you to add your own attribute methods with
   prefix suffix and affix names. For more information check out
   ActiveModel::AttributeMethods::ClassMethods
This commit is contained in:
Will Leinweber 2010-09-16 17:24:43 -05:00
parent 5c21de8586
commit d333133319
12 changed files with 333 additions and 124 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ pkg
Gemfile* Gemfile*
.rvmrc .rvmrc
.bundle .bundle
couchdb.std*

View file

@ -1,6 +1,8 @@
== Next Version == Next Version
* Major enhancements * Major enhancements
* Dirty Tracking via ActiveModel
* ActiveModel Attribute Methods support
* Minor enhancements * Minor enhancements
* Fixing find("") issue (thanks epochwolf) * Fixing find("") issue (thanks epochwolf)

View file

@ -1 +1 @@
require File.join(File.dirname(__FILE__),'lib', 'couchrest', 'extended_document') require File.join(File.dirname(__FILE__),'lib', 'couchrest', 'model')

View file

@ -1,17 +1,66 @@
module CouchRest module CouchRest
module Model module Model
ReadOnlyPropertyError = Class.new(StandardError)
# Attributes Suffixes provide methods from ActiveModel
# to hook into. See methods such as #attribute= and
# #attribute? for their implementation
AttributeMethodSuffixes = ['', '=', '?']
module Attributes module Attributes
extend ActiveSupport::Concern
included do
include ActiveModel::AttributeMethods
attribute_method_suffix *AttributeMethodSuffixes
end
module ClassMethods
def attributes
properties.map {|prop| prop.name}
end
end
def initialize(*args)
self.class.attribute_method_suffix *AttributeMethodSuffixes
super
end
def attributes
self.class.attributes
end
## Reads the attribute value.
# Assuming you have a property :title this would be called
# by `model_instance.title`
def attribute(name)
read_attribute(name)
end
## Sets the attribute value.
# Assuming you have a property :title this would be called
# by `model_instance.title = 'hello'`
def attribute=(name, value)
raise ReadOnlyPropertyError, 'read only property' if find_property!(name).read_only
write_attribute(name, value)
end
## Tests for both presence and truthiness of the attribute.
# Assuming you have a property :title # this would be called
# by `model_instance.title?`
def attribute?(name)
value = read_attribute(name)
!(value.nil? || value == false)
end
## Support for handling attributes ## Support for handling attributes
# #
# This would be better in the properties file, but due to scoping issues # This would be better in the properties file, but due to scoping issues
# this is not yet possible. # this is not yet possible.
#
def prepare_all_attributes(doc = {}, options = {}) def prepare_all_attributes(doc = {}, options = {})
apply_all_property_defaults apply_all_property_defaults
if options[:directly_set_attributes] if options[:directly_set_attributes]
directly_set_read_only_attributes(doc) directly_set_read_only_attributes(doc)
else else
remove_protected_attributes(doc) remove_protected_attributes(doc)
end end
@ -20,7 +69,7 @@ module CouchRest
# Takes a hash as argument, and applies the values by using writer methods # 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 # 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. # missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash) def update_attributes_without_saving(hash)
# Remove any protected and update all the rest. Any attributes # Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored. # which do not have a property will simply be ignored.
@ -29,11 +78,26 @@ module CouchRest
end end
alias :attributes= :update_attributes_without_saving alias :attributes= :update_attributes_without_saving
def read_attribute(property)
prop = find_property!(property)
self[prop.to_s]
end
def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.cast(self, value)
end
private private
def read_only_attributes
properties.select { |prop| prop.read_only }.map { |prop| prop.name }
end
def directly_set_attributes(hash) def directly_set_attributes(hash)
r_o_a = read_only_attributes
hash.each do |attribute_name, attribute_value| hash.each do |attribute_name, attribute_value|
next if r_o_a.include? attribute_name
if self.respond_to?("#{attribute_name}=") if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", hash.delete(attribute_name)) self.send("#{attribute_name}=", hash.delete(attribute_name))
end end
@ -41,27 +105,27 @@ module CouchRest
end end
def directly_set_read_only_attributes(hash) def directly_set_read_only_attributes(hash)
property_list = self.properties.map{|p| p.name} r_o_a = read_only_attributes
property_list = attributes
hash.each do |attribute_name, attribute_value| hash.each do |attribute_name, attribute_value|
next if self.respond_to?("#{attribute_name}=") next unless r_o_a.include? attribute_name
if property_list.include?(attribute_name) if property_list.include?(attribute_name)
write_attribute(attribute_name, hash.delete(attribute_name)) write_attribute(attribute_name, hash.delete(attribute_name))
end end
end end
end end
def set_attributes(hash) def set_attributes(hash)
attrs = remove_protected_attributes(hash) attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs) directly_set_attributes(attrs)
end end
def check_properties_exist(attrs) def check_properties_exist(attrs)
property_list = self.properties.map{|p| p.name} property_list = attributes
attrs.each do |attribute_name, attribute_value| attrs.each do |attribute_name, attribute_value|
raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name) raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
end end
end end
end end
end end
end end

View file

@ -6,7 +6,7 @@ module CouchRest
include CouchRest::Model::Persistence include CouchRest::Model::Persistence
include CouchRest::Model::Callbacks include CouchRest::Model::Callbacks
include CouchRest::Model::DocumentQueries include CouchRest::Model::DocumentQueries
include CouchRest::Model::Views include CouchRest::Model::Views
include CouchRest::Model::DesignDoc include CouchRest::Model::DesignDoc
include CouchRest::Model::ExtendedAttachments include CouchRest::Model::ExtendedAttachments
@ -16,11 +16,12 @@ module CouchRest
include CouchRest::Model::Attributes include CouchRest::Model::Attributes
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Dirty
def self.subclasses def self.subclasses
@subclasses ||= [] @subclasses ||= []
end end
def self.inherited(subklass) def self.inherited(subklass)
super super
subklass.send(:include, CouchRest::Model::Properties) subklass.send(:include, CouchRest::Model::Properties)
@ -34,16 +35,16 @@ module CouchRest
EOS EOS
subclasses << subklass subclasses << subklass
end end
# Accessors # Accessors
attr_accessor :casted_by attr_accessor :casted_by
# Instantiate a new CouchRest::Model::Base by preparing all properties # Instantiate a new CouchRest::Model::Base by preparing all properties
# using the provided document hash. # using the provided document hash.
# #
# Options supported: # Options supported:
# #
# * :directly_set_attributes: true when data comes directly from database # * :directly_set_attributes: true when data comes directly from database
# #
def initialize(doc = {}, options = {}) def initialize(doc = {}, options = {})
@ -54,8 +55,8 @@ module CouchRest
end end
after_initialize if respond_to?(:after_initialize) after_initialize if respond_to?(:after_initialize)
end end
# Temp solution to make the view_by methods available # Temp solution to make the view_by methods available
def self.method_missing(m, *args, &block) def self.method_missing(m, *args, &block)
if has_view?(m) if has_view?(m)
@ -69,9 +70,9 @@ module CouchRest
end end
super super
end end
### instance methods ### instance methods
# Gets a reference to the actual document in the DB # Gets a reference to the actual document in the DB
# Calls up to the next document if there is one, # Calls up to the next document if there is one,
# Otherwise we're at the top and we return self # Otherwise we're at the top and we return self
@ -79,14 +80,14 @@ module CouchRest
return self if base_doc? return self if base_doc?
@casted_by.base_doc @casted_by.base_doc
end end
# Checks if we're the top document # Checks if we're the top document
def base_doc? def base_doc?
!@casted_by !@casted_by
end end
## Compatibility with ActiveSupport and older frameworks ## Compatibility with ActiveSupport and older frameworks
# Hack so that CouchRest::Document, which descends from Hash, # Hack so that CouchRest::Document, which descends from Hash,
# doesn't appear to Rails routing as a Hash of options # doesn't appear to Rails routing as a Hash of options
def is_a?(klass) def is_a?(klass)
@ -98,14 +99,14 @@ module CouchRest
def persisted? def persisted?
!new? !new?
end end
def to_key def to_key
new? ? nil : [id] new? ? nil : [id]
end end
alias :to_param :id alias :to_param :id
alias :new_record? :new? alias :new_record? :new?
alias :new_document? :new? alias :new_document? :new?
end end
end end
end end

View file

@ -1,6 +1,6 @@
module CouchRest::Model module CouchRest::Model
module CastedModel module CastedModel
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
@ -12,28 +12,28 @@ module CouchRest::Model
include CouchRest::Model::Validations include CouchRest::Model::Validations
attr_accessor :casted_by attr_accessor :casted_by
end end
def initialize(keys = {}) def initialize(keys = {})
raise StandardError unless self.is_a? Hash raise StandardError unless self.is_a? Hash
prepare_all_attributes(keys) prepare_all_attributes(keys)
super() super()
end end
def []= key, value def []= key, value
super(key.to_s, value) super(key.to_s, value)
end end
def [] key def [] key
super(key.to_s) super(key.to_s)
end end
# Gets a reference to the top level extended # Gets a reference to the top level extended
# document that a model is saved inside of # document that a model is saved inside of
def base_doc def base_doc
return nil unless @casted_by return nil unless @casted_by
@casted_by.base_doc @casted_by.base_doc
end end
# False if the casted model has already # False if the casted model has already
# been saved in the containing document # been saved in the containing document
def new? def new?
@ -53,12 +53,12 @@ module CouchRest::Model
end end
alias :to_key :id alias :to_key :id
alias :to_param :id alias :to_param :id
# Sets the attributes from a hash # Sets the attributes from a hash
def update_attributes_without_saving(hash) def update_attributes_without_saving(hash)
hash.each do |k, v| hash.each do |k, v|
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
end end
hash.each do |k, v| hash.each do |k, v|
self.send("#{k}=",v) self.send("#{k}=",v)
end end

View file

@ -0,0 +1,44 @@
# encoding: utf-8
require 'active_model/dirty'
module CouchRest #:nodoc:
module Model #:nodoc:
# Dirty Tracking support via ActiveModel
# mixin methods include:
# #changed?, #changed, #changes, #previous_changes
# #<attribute>_changed?, #<attribute>_change,
# #reset_<attribute>!, #<attribute>_will_change!,
# and #<attribute>_was
#
# Please see the specs or the documentation of
# ActiveModel::Dirty for more information
module Dirty
extend ActiveSupport::Concern
included do
include ActiveModel::Dirty
after_save :clear_changed_attributes
end
def initialize(*args)
super
@changed_attributes.clear if @changed_attributes
end
def write_attribute(name, value)
meth = :"#{name}_will_change!"
__send__ meth if respond_to? meth
super
end
private
def clear_changed_attributes
@previously_changed = changes
@changed_attributes.clear
true
end
end
end
end

View file

@ -1,4 +1,5 @@
# encoding: utf-8 # encoding: utf-8
require 'set'
module CouchRest module CouchRest
module Model module Model
module Properties module Properties
@ -22,16 +23,6 @@ module CouchRest
self.class.properties self.class.properties
end end
def read_attribute(property)
prop = find_property!(property)
self[prop.to_s]
end
def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.cast(self, value)
end
def apply_all_property_defaults def apply_all_property_defaults
return if self.respond_to?(:new?) && (new? == false) return if self.respond_to?(:new?) && (new? == false)
# TODO: cache the default object # TODO: cache the default object
@ -94,8 +85,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_alias(property) if property.alias
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
end end
@ -103,49 +93,15 @@ module CouchRest
property property
end end
# defines the getter for the property (and optional aliases) def create_property_alias(property)
def create_property_getter(property)
# meth = property.name
class_eval <<-EOS, __FILE__, __LINE__ + 1 class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{property.name} def #{property.alias.to_s}
read_attribute('#{property.name}') #{property.name}
end end
EOS EOS
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
class_eval <<-EOS, __FILE__, __LINE__
def #{property.name}?
value = read_attribute('#{property.name}')
!(value.nil? || value == false)
end
EOS
end
if property.alias
class_eval <<-EOS, __FILE__, __LINE__ + 1
alias #{property.alias.to_sym} #{property.name.to_sym}
EOS
end
end
# defines the setter for the property (and optional aliases)
def create_property_setter(property)
property_name = property.name
class_eval <<-EOS
def #{property_name}=(value)
write_attribute('#{property_name}', value)
end
EOS
if property.alias
class_eval <<-EOS
alias #{property.alias.to_sym}= #{property_name.to_sym}=
EOS
end
end end
end # module ClassMethods end # module ClassMethods
end end
end end
end end

View file

@ -48,6 +48,7 @@ require "couchrest/model/collection"
require "couchrest/model/attribute_protection" require "couchrest/model/attribute_protection"
require "couchrest/model/attributes" require "couchrest/model/attributes"
require "couchrest/model/associations" require "couchrest/model/associations"
require "couchrest/model/dirty"
# Monkey patches applied to couchrest # Monkey patches applied to couchrest
require "couchrest/model/support/couchrest" require "couchrest/model/support/couchrest"

View file

@ -8,23 +8,23 @@ require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base') require File.join(FIXTURE_PATH, 'base')
describe "Model Base" do describe "Model Base" do
before(:each) do before(:each) do
@obj = WithDefaultValues.new @obj = WithDefaultValues.new
end end
describe "instance database connection" do describe "instance database connection" do
it "should use the default database" do it "should use the default database" do
@obj.database.name.should == 'couchrest-model-test' @obj.database.name.should == 'couchrest-model-test'
end end
it "should override the default db" do it "should override the default db" do
@obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test') @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test')
@obj.database.name.should == 'couchrest-extendedmodel-test' @obj.database.name.should == 'couchrest-extendedmodel-test'
@obj.database.delete! @obj.database.delete!
end end
end end
describe "a new model" do describe "a new model" do
it "should be a new document" do it "should be a new document" do
@obj = Basic.new @obj = Basic.new
@ -39,10 +39,10 @@ describe "Model Base" do
@obj.should == { 'couchrest-type' => 'Basic' } @obj.should == { 'couchrest-type' => 'Basic' }
end end
end end
describe "ActiveModel compatability Basic" do describe "ActiveModel compatability Basic" do
before(:each) do before(:each) do
@obj = Basic.new(nil) @obj = Basic.new(nil)
end end
@ -86,7 +86,7 @@ describe "Model Base" do
context "when the document is not new" do context "when the document is not new" do
it "returns id" do it "returns id" do
@obj.save @obj.save
@obj.persisted?.should == true @obj.persisted?.should == true
end end
end end
end end
@ -100,7 +100,7 @@ describe "Model Base" do
end end
describe "update attributes without saving" do describe "update attributes without saving" do
before(:each) do before(:each) do
a = Article.get "big-bad-danger" rescue nil a = Article.get "big-bad-danger" rescue nil
@ -134,22 +134,22 @@ describe "Model Base" do
@art.attributes = {'date' => Time.now, :title => "something else"} @art.attributes = {'date' => Time.now, :title => "something else"}
@art['title'].should == "something else" @art['title'].should == "something else"
end end
it "should not flip out if an attribute= method is missing and ignore it" do it "should not flip out if an attribute= method is missing and ignore it" do
lambda { lambda {
@art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
}.should_not raise_error }.should_not raise_error
@art.slug.should == "big-bad-danger" @art.slug.should == "big-bad-danger"
end end
#it "should not change other attributes if there is an error" do #it "should not change other attributes if there is an error" do
# lambda { # lambda {
# @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") # @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
# }.should raise_error # }.should raise_error
# @art['title'].should == "big bad danger" # @art['title'].should == "big bad danger"
#end #end
end end
describe "update attributes" do describe "update attributes" do
before(:each) do before(:each) do
a = Article.get "big-bad-danger" rescue nil a = Article.get "big-bad-danger" rescue nil
@ -164,7 +164,7 @@ describe "Model Base" do
loaded['title'].should == "super danger" loaded['title'].should == "super danger"
end end
end end
describe "with default" do describe "with default" do
it "should have the default value set at initalization" do it "should have the default value set at initalization" do
@obj.preset.should == {:right => 10, :top_align => false} @obj.preset.should == {:right => 10, :top_align => false}
@ -173,23 +173,23 @@ describe "Model Base" do
it "should have the default false value explicitly assigned" do it "should have the default false value explicitly assigned" do
@obj.default_false.should == false @obj.default_false.should == false
end end
it "should automatically call a proc default at initialization" do it "should automatically call a proc default at initialization" do
@obj.set_by_proc.should be_an_instance_of(Time) @obj.set_by_proc.should be_an_instance_of(Time)
@obj.set_by_proc.should == @obj.set_by_proc @obj.set_by_proc.should == @obj.set_by_proc
@obj.set_by_proc.should < Time.now @obj.set_by_proc.should < Time.now
end end
it "should let you overwrite the default values" do it "should let you overwrite the default values" do
obj = WithDefaultValues.new(:preset => 'test') obj = WithDefaultValues.new(:preset => 'test')
obj.preset = 'test' obj.preset = 'test'
end end
it "should work with a default empty array" do it "should work with a default empty array" do
obj = WithDefaultValues.new(:tags => ['spec']) obj = WithDefaultValues.new(:tags => ['spec'])
obj.tags.should == ['spec'] obj.tags.should == ['spec']
end end
it "should set default value of read-only property" do it "should set default value of read-only property" do
obj = WithDefaultValues.new obj = WithDefaultValues.new
obj.read_only_with_default.should == 'generic' obj.read_only_with_default.should == 'generic'
@ -207,7 +207,7 @@ describe "Model Base" do
obj.tags.should == ['spec'] obj.tags.should == ['spec']
end end
end end
describe "a doc with template values (CR::Model spec)" do describe "a doc with template values (CR::Model spec)" do
before(:all) do before(:all) do
WithTemplateAndUniqueID.all.map{|o| o.destroy} WithTemplateAndUniqueID.all.map{|o| o.destroy}
@ -228,8 +228,8 @@ describe "Model Base" do
tmpl2_reloaded.preset.should == 'not_value' tmpl2_reloaded.preset.should == 'not_value'
end end
end end
describe "finding all instances of a model" do describe "finding all instances of a model" do
before(:all) do before(:all) do
WithTemplateAndUniqueID.req_design_doc_refresh WithTemplateAndUniqueID.req_design_doc_refresh
@ -246,32 +246,32 @@ describe "Model Base" do
d['views']['all']['map'].should include('WithTemplateAndUniqueID') d['views']['all']['map'].should include('WithTemplateAndUniqueID')
end end
it "should find all" do it "should find all" do
rs = WithTemplateAndUniqueID.all rs = WithTemplateAndUniqueID.all
rs.length.should == 4 rs.length.should == 4
end end
end end
describe "counting all instances of a model" do describe "counting all instances of a model" do
before(:each) do before(:each) do
@db = reset_test_db! @db = reset_test_db!
WithTemplateAndUniqueID.req_design_doc_refresh WithTemplateAndUniqueID.req_design_doc_refresh
end end
it ".count should return 0 if there are no docuemtns" do it ".count should return 0 if there are no docuemtns" do
WithTemplateAndUniqueID.count.should == 0 WithTemplateAndUniqueID.count.should == 0
end end
it ".count should return the number of documents" do it ".count should return the number of documents" do
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('important-field' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save WithTemplateAndUniqueID.new('important-field' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save WithTemplateAndUniqueID.new('important-field' => '3').save
WithTemplateAndUniqueID.count.should == 3 WithTemplateAndUniqueID.count.should == 3
end end
end end
describe "finding the first instance of a model" do describe "finding the first instance of a model" do
before(:each) do before(:each) do
@db = reset_test_db! @db = reset_test_db!
# WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
WithTemplateAndUniqueID.new('important-field' => '1').save WithTemplateAndUniqueID.new('important-field' => '1').save
@ -309,7 +309,7 @@ describe "Model Base" do
WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev) WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
end end
end end
describe "getting a model with a subobject field" do describe "getting a model with a subobject field" do
before(:all) do before(:all) do
course_doc = { course_doc = {
@ -332,7 +332,7 @@ describe "Model Base" do
@course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800") @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800")
end end
end end
describe "timestamping" do describe "timestamping" do
before(:each) do before(:each) do
oldart = Article.get "saving-this" rescue nil oldart = Article.get "saving-this" rescue nil
@ -340,7 +340,7 @@ describe "Model Base" do
@art = Article.new(:title => "Saving this") @art = Article.new(:title => "Saving this")
@art.save @art.save
end end
it "should define the updated_at and created_at getters and set the values" do it "should define the updated_at and created_at getters and set the values" do
@obj.save @obj.save
obj = WithDefaultValues.get(@obj.id) obj = WithDefaultValues.get(@obj.id)
@ -349,15 +349,15 @@ describe "Model Base" do
obj.updated_at.should be_an_instance_of(Time) obj.updated_at.should be_an_instance_of(Time)
obj.created_at.to_s.should == @obj.updated_at.to_s obj.created_at.to_s.should == @obj.updated_at.to_s
end end
it "should not change created_at on update" do it "should not change created_at on update" do
2.times do 2.times do
lambda do lambda do
@art.save @art.save
end.should_not change(@art, :created_at) end.should_not change(@art, :created_at)
end end
end end
it "should set the time on create" do it "should set the time on create" do
(Time.now - @art.created_at).should < 2 (Time.now - @art.created_at).should < 2
foundart = Article.get @art.id foundart = Article.get @art.id
@ -368,7 +368,7 @@ describe "Model Base" do
@art.created_at.should < @art.updated_at @art.created_at.should < @art.updated_at
end end
end end
describe "getter and setter methods" do describe "getter and setter methods" do
it "should try to call the arg= method before setting :arg in the hash" do it "should try to call the arg= method before setting :arg in the hash" do
@doc = WithGetterAndSetterMethods.new(:arg => "foo") @doc = WithGetterAndSetterMethods.new(:arg => "foo")
@ -384,41 +384,41 @@ describe "Model Base" do
@doc['some_value'].should eql('value') @doc['some_value'].should eql('value')
end end
end end
describe "recursive validation on a model" do describe "recursive validation on a model" do
before :each do before :each do
reset_test_db! reset_test_db!
@cat = Cat.new(:name => 'Sockington') @cat = Cat.new(:name => 'Sockington')
end end
it "should not save if a nested casted model is invalid" do it "should not save if a nested casted model is invalid" do
@cat.favorite_toy = CatToy.new @cat.favorite_toy = CatToy.new
@cat.should_not be_valid @cat.should_not be_valid
@cat.save.should be_false @cat.save.should be_false
lambda{@cat.save!}.should raise_error lambda{@cat.save!}.should raise_error
end end
it "should save when nested casted model is valid" do it "should save when nested casted model is valid" do
@cat.favorite_toy = CatToy.new(:name => 'Squeaky') @cat.favorite_toy = CatToy.new(:name => 'Squeaky')
@cat.should be_valid @cat.should be_valid
@cat.save.should be_true @cat.save.should be_true
lambda{@cat.save!}.should_not raise_error lambda{@cat.save!}.should_not raise_error
end end
it "should not save when nested collection contains an invalid casted model" do it "should not save when nested collection contains an invalid casted model" do
@cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new] @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
@cat.should_not be_valid @cat.should_not be_valid
@cat.save.should be_false @cat.save.should be_false
lambda{@cat.save!}.should raise_error lambda{@cat.save!}.should raise_error
end end
it "should save when nested collection contains valid casted models" do it "should save when nested collection contains valid casted models" do
@cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')] @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
@cat.should be_valid @cat.should be_valid
@cat.save.should be_true @cat.save.should be_true
lambda{@cat.save!}.should_not raise_error lambda{@cat.save!}.should_not raise_error
end end
it "should not fail if the nested casted model doesn't have validation" do it "should not fail if the nested casted model doesn't have validation" do
Cat.property :trainer, Person Cat.property :trainer, Person
Cat.validates_presence_of :name Cat.validates_presence_of :name

View file

@ -0,0 +1,126 @@
require File.expand_path("../../spec_helper", __FILE__)
class DirtyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :color
validates_presence_of :name
end
describe 'Dirty Tracking', '#changed?' do
before(:each) do
@dm = DirtyModel.new
@dm.name = 'will'
end
it 'brand new models should not be changed by default' do
DirtyModel.new.should_not be_changed
end
it 'save should reset changed?' do
@dm.should be_changed
@dm.save
@dm.should_not be_changed
end
it 'save! should reset changed?' do
@dm.should be_changed
@dm.save!
@dm.should_not be_changed
end
it 'a failed save should preserve changed?' do
@dm.name = ''
@dm.should be_changed
@dm.save.should be_false
@dm.should be_changed
end
it 'should be true if there have been changes' do
@dm.name = 'not will'
@dm.should be_changed
end
end
describe 'Dirty Tracking', '#changed' do
it 'should be an array of the changed attributes' do
dm = DirtyModel.new
dm.changed.should == []
dm.name = 'will'
dm.changed.should == ['name']
dm.color = 'red'
dm.changed.should =~ ['name', 'color']
end
end
describe 'Dirty Tracking', '#changes' do
it 'should be a Map of changed attrs => [original value, new value]' do
dm = DirtyModel.new(:name => 'will', :color => 'red')
dm.save!
dm.should_not be_changed
dm.name = 'william'
dm.color = 'blue'
dm.changes.should == { 'name' => ['will', 'william'], 'color' => ['red', 'blue'] }
end
end
describe 'Dirty Tracking', '#previous_changes' do
it 'should store the previous changes after a save' do
dm = DirtyModel.new(:name => 'will', :color => 'red')
dm.save!
dm.should_not be_changed
dm.name = 'william'
dm.save!
dm.previous_changes.should == { 'name' => ['will', 'william'] }
end
end
describe 'Dirty Tracking', 'attribute methods' do
before(:each) do
@dm = DirtyModel.new(:name => 'will', :color => 'red')
@dm.save!
end
describe '#<attr>_changed?' do
it 'it should know if a specific property was changed' do
@dm.name = 'william'
@dm.should be_name_changed
@dm.should_not be_color_changed
end
end
describe 'Dirty Tracking', '#<attr>_change' do
it 'should be an array of [original value, current value]' do
@dm.name = 'william'
@dm.name_change.should == ['will', 'william']
end
end
describe 'Dirty Tracking', '#<attr>_was' do
it 'should return what the attribute was' do
@dm.name = 'william'
@dm.name_was.should == 'will'
end
end
describe 'Dirty Tracking', '#reset_<attr>!' do
it 'should reset the attribute to what it was' do
@dm.name = 'william'
@dm.reset_name!
@dm.name.should == 'will'
end
end
describe 'Dirty Tracking', '#<attr>_will_change!' do
it 'should manually mark the attribute as changed' do
@dm.should_not be_name_changed
@dm.name_will_change!
@dm.should be_name_changed
end
end
end

View file

@ -9,6 +9,20 @@ require File.join(FIXTURE_PATH, 'more', 'event')
require File.join(FIXTURE_PATH, 'more', 'user') require File.join(FIXTURE_PATH, 'more', 'user')
require File.join(FIXTURE_PATH, 'more', 'course') require File.join(FIXTURE_PATH, 'more', 'course')
describe 'Attributes' do
class AttrDoc < CouchRest::Model::Base
property :one
property :two
end
it '.attributes should have an array of attribute names' do
AttrDoc.attributes.should =~ ['two', 'one']
end
it '#attributes should have an array of attribute names' do
AttrDoc.new.attributes.should =~ ['two', 'one']
end
end
describe "Model properties" do describe "Model properties" do