Merge branch 'master' of https://github.com/2moro/couchrest_model into 2moro-dirty

Conflicts:
	.gitignore
	lib/couchrest/model/base.rb
	lib/couchrest/model/configuration.rb
	lib/couchrest_model.rb
This commit is contained in:
Sam Lown 2011-04-20 10:47:36 +02:00
commit 1bced3b207
18 changed files with 819 additions and 41 deletions

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ pkg
couchdb.std* couchdb.std*
*.*~ *.*~
Gemfile.lock Gemfile.lock
spec.out

118
benchmarks/dirty.rb Normal file
View file

@ -0,0 +1,118 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'benchmark'
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'couchrest_model'
class BenchmarkCasted < Hash
include CouchRest::Model::CastedModel
property :name
end
class BenchmarkModel < CouchRest::Model::Base
use_database CouchRest.database!(ENV['BENCHMARK_DB'] || "http://localhost:5984/test")
property :string, String
property :number, Integer
property :casted, BenchmarkCasted
property :casted_list, [BenchmarkCasted]
end
# set dirty configuration, return previous configuration setting
def set_dirty(value)
orig = nil
CouchRest::Model::Base.configure do |config|
orig = config.use_dirty
config.use_dirty = value
end
BenchmarkModel.instance_eval do
self.use_dirty = value
end
orig
end
def supports_dirty?
CouchRest::Model::Base.respond_to?(:use_dirty)
end
def run_benchmark
n = 50000 # property operation count
db_n = 1000 # database operation count
b = BenchmarkModel.new
Benchmark.bm(30) do |x|
# property assigning
x.report("assign string:") do
n.times { b.string = "test" }
end
next if ENV["BENCHMARK_STRING"]
x.report("assign integer:") do
n.times { b.number = 1 }
end
x.report("assign casted hash:") do
n.times { b.casted = { 'name' => 'test' } }
end
x.report("assign casted hash list:") do
n.times { b.casted_list = [{ 'name' => 'test' }] }
end
# property reading
x.report("read string") do
n.times { b.string }
end
x.report("read integer") do
n.times { b.number }
end
x.report("read casted hash") do
n.times { b.casted }
end
x.report("read casted hash list") do
n.times { b.casted_list }
end
if ENV['BENCHMARK_DB']
# db writing
x.report("write changed record to db") do
db_n.times { |i| b.string = "test#{i}"; b.save }
end
x.report("write unchanged record to db") do
db_n.times { b.save }
end
# db reading
x.report("read record from db") do
db_n.times { BenchmarkModel.find(b.id) }
end
end
end
end
begin
if supports_dirty?
if !ENV['BENCHMARK_DIRTY_OFF']
set_dirty(true)
puts "with use_dirty true"
run_benchmark
end
set_dirty(false)
end
puts "\nwith use_dirty false"
run_benchmark
end

View file

@ -18,14 +18,17 @@ module CouchRest
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Designs include CouchRest::Model::Designs
include CouchRest::Model::Dirty
include CouchRest::Model::CastedBy
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)
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1 subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.inherited(subklass) def self.inherited(subklass)
super super
@ -36,7 +39,7 @@ module CouchRest
EOS EOS
subclasses << subklass subclasses << subklass
end end
# Accessors # Accessors
attr_accessor :casted_by attr_accessor :casted_by
@ -45,7 +48,7 @@ module CouchRest
# 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
# * :database: provide an alternative database # * :database: provide an alternative database
# #
@ -59,8 +62,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)
@ -74,24 +77,17 @@ module CouchRest
end end
super super
end end
### instance methods ### instance methods
# Gets a reference to the actual document in the DB
# Calls up to the next document if there is one,
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by.base_doc
end
# Checks if we're the top document # Checks if we're the top document
# (overrides base_doc? in casted_by.rb)
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)
@ -103,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

@ -5,6 +5,7 @@
module CouchRest::Model module CouchRest::Model
class CastedArray < Array class CastedArray < Array
include CouchRest::Model::Dirty
attr_accessor :casted_by attr_accessor :casted_by
attr_accessor :property attr_accessor :property
@ -14,15 +15,39 @@ module CouchRest::Model
end end
def << obj def << obj
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj)) super(instantiate_and_cast(obj))
end end
def push(obj) def push(obj)
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def pop
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
end
def shift
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
end
def unshift(obj)
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj)) super(instantiate_and_cast(obj))
end end
def []= index, obj def []= index, obj
super(index, instantiate_and_cast(obj)) value = instantiate_and_cast(obj)
couchrest_parent_will_change! if use_dirty? && value != self[index]
super(index, value)
end
def clear
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
end end
protected protected

View file

@ -0,0 +1,23 @@
module CouchRest::Model
module CastedBy
extend ActiveSupport::Concern
included do
self.send(:attr_accessor, :casted_by)
end
# Gets a reference to the actual document in the DB
# Calls up to the next document if there is one,
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by ? @casted_by.base_doc : nil
end
# Checks if we're the top document
def base_doc?
false
end
end
end

View file

@ -0,0 +1,76 @@
#
# Wrapper around Hash so that the casted_by attribute is set.
module CouchRest::Model
class CastedHash < Hash
include CouchRest::Model::Dirty
attr_accessor :casted_by
# needed for dirty
def attributes
self
end
def []= key, obj
couchrest_attribute_will_change!(key) if use_dirty? && obj != self[key]
super(key, obj)
end
def delete(key)
couchrest_attribute_will_change!(key) if use_dirty? && include?(key)
super(key)
end
def merge!(other_hash)
if use_dirty? && other_hash && other_hash.kind_of?(Hash)
other_hash.keys.each do |key|
if self[key] != other_hash[key] || !include?(key)
couchrest_attribute_will_change!(key)
end
end
end
super(other_hash)
end
def replace(other_hash)
if use_dirty? && other_hash && other_hash.kind_of?(Hash)
# new keys and changed keys
other_hash.keys.each do |key|
if self[key] != other_hash[key] || !include?(key)
couchrest_attribute_will_change!(key)
end
end
# old keys
old_keys = self.keys.reject { |key| other_hash.include?(key) }
old_keys.each { |key| couchrest_attribute_will_change!(key) }
end
super(other_hash)
end
def clear
self.keys.each { |key| couchrest_attribute_will_change!(key) } if use_dirty?
super
end
def delete_if
if use_dirty? && block_given?
self.keys.each do |key|
couchrest_attribute_will_change!(key) if yield key, self[key]
end
end
super
end
# ruby 1.9
def keep_if
if use_dirty? && block_given?
self.keys.each do |key|
couchrest_attribute_will_change!(key) if !yield key, self[key]
end
end
super
end
end
end

View file

@ -10,6 +10,7 @@ module CouchRest::Model
include CouchRest::Model::PropertyProtection include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Dirty
attr_accessor :casted_by attr_accessor :casted_by
end end
@ -20,6 +21,7 @@ module CouchRest::Model
end end
def []= key, value def []= key, value
couchrest_attribute_will_change!(key) if use_dirty && self[key] != value
super(key.to_s, value) super(key.to_s, value)
end end
@ -64,5 +66,6 @@ module CouchRest::Model
end end
end end
alias :attributes= :update_attributes_without_saving alias :attributes= :update_attributes_without_saving
end end
end end

View file

@ -11,11 +11,13 @@ module CouchRest
add_config :model_type_key add_config :model_type_key
add_config :mass_assign_any_attribute add_config :mass_assign_any_attribute
add_config :auto_update_design_doc add_config :auto_update_design_doc
add_config :use_dirty
configure do |config| configure do |config|
config.model_type_key = 'model' # was 'couchrest-type' config.model_type_key = 'model' # was 'couchrest-type'
config.mass_assign_any_attribute = false config.mass_assign_any_attribute = false
config.auto_update_design_doc = true config.auto_update_design_doc = true
config.use_dirty = true
end end
end end
@ -49,5 +51,3 @@ module CouchRest
end end
end end
end end

View file

@ -0,0 +1,49 @@
# encoding: utf-8
I18n.load_path << File.join(
File.dirname(__FILE__), "validations", "locale", "en.yml"
)
module CouchRest
module Model
# This applies to both Model::Base and Model::CastedModel
module Dirty
extend ActiveSupport::Concern
include CouchRest::Model::CastedBy # needed for base_doc
include ActiveModel::Dirty
included do
# internal dirty setting - overrides global setting.
# this is used to temporarily disable dirty tracking when setting
# attributes directly, for performance reasons.
self.send(:attr_accessor, :disable_dirty)
end
def use_dirty?
bdoc = base_doc
bdoc && bdoc.use_dirty && !bdoc.disable_dirty
end
def couchrest_attribute_will_change!(attr)
return if attr.nil? || !use_dirty?
attribute_will_change!(attr)
couchrest_parent_will_change!
end
def couchrest_parent_will_change!
@casted_by.couchrest_attribute_will_change!(casted_by_attribute) if @casted_by
end
private
# return the attribute name this object is referenced by in the parent
def casted_by_attribute
return @casted_by_attribute if @casted_by_attribute
attr = @casted_by.attributes
@casted_by_attribute = attr.keys.detect { |k| attr[k] == self }
end
end
end
end

View file

@ -1,6 +1,7 @@
module CouchRest module CouchRest
module Model module Model
module ExtendedAttachments module ExtendedAttachments
extend ActiveSupport::Concern
# Add a file attachment to the current document. Expects # Add a file attachment to the current document. Expects
# :file and :name to be included in the arguments. # :file and :name to be included in the arguments.
@ -35,7 +36,10 @@ module CouchRest
# deletes a file attachment from the current doc # deletes a file attachment from the current doc
def delete_attachment(attachment_name) def delete_attachment(attachment_name)
return unless attachments return unless attachments
attachments.delete attachment_name if attachments.include?(attachment_name)
attribute_will_change!("_attachments")
attachments.delete attachment_name
end
end end
# returns true if attachment_name exists # returns true if attachment_name exists
@ -66,6 +70,8 @@ module CouchRest
def set_attachment_attr(args) def set_attachment_attr(args)
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path) content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
content_type ||= (get_mime_type(args[:name]) || 'text/plain') content_type ||= (get_mime_type(args[:name]) || 'text/plain')
attribute_will_change!("_attachments")
attachments[args[:name]] = { attachments[args[:name]] = {
'content_type' => content_type, 'content_type' => content_type,
'data' => args[:file].read 'data' => args[:file].read

View file

@ -12,7 +12,9 @@ module CouchRest
_run_save_callbacks do _run_save_callbacks do
set_unique_id if new? && self.respond_to?(:set_unique_id) set_unique_id if new? && self.respond_to?(:set_unique_id)
result = database.save_doc(self) result = database.save_doc(self)
(result["ok"] == true) ? self : false ret = (result["ok"] == true) ? self : false
@changed_attributes.clear if ret && @changed_attributes
ret
end end
end end
end end
@ -28,10 +30,13 @@ module CouchRest
def update(options = {}) def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new? raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
return false unless perform_validations(options) return false unless perform_validations(options)
return true if use_dirty? && !self.changed?
_run_update_callbacks do _run_update_callbacks do
_run_save_callbacks do _run_save_callbacks do
result = database.save_doc(self) result = database.save_doc(self)
result["ok"] == true ret = result["ok"] == true
@changed_attributes.clear if ret && @changed_attributes
ret
end end
end end
end end
@ -140,12 +145,18 @@ module CouchRest
# should use the class name as part of the unique id. # should use the class name as part of the unique id.
def unique_id method = nil, &block def unique_id method = nil, &block
if method if method
define_method :get_unique_id do
self.send(method)
end
define_method :set_unique_id do define_method :set_unique_id do
self['_id'] ||= self.send(method) self['_id'] ||= get_unique_id
end end
elsif block elsif block
define_method :get_unique_id do
block.call(self)
end
define_method :set_unique_id do define_method :set_unique_id do
uniqid = block.call(self) uniqid = get_unique_id
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil? raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
self['_id'] ||= uniqid self['_id'] ||= uniqid
end end

View file

@ -6,7 +6,9 @@ module CouchRest
included do included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
extlib_inheritable_accessor(:prop_by_name) unless self.respond_to?(:prop_by_name)
self.properties ||= [] self.properties ||= []
self.prop_by_name ||= {}
raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=)) raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
end end
@ -43,6 +45,36 @@ module CouchRest
self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value) self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
end end
# write property, update dirty status
def write_attribute_dirty(property, value)
prop = find_property!(property)
value = prop.is_a?(String) ? value : prop.cast(self, value)
propname = prop.to_s
attribute_will_change!(propname) if use_dirty? && self[propname] != value
self[propname] = value
end
def []=(key,value)
return super(key,value) unless use_dirty?
has_changes = self.changed?
if !has_changes && self.respond_to?(:get_unique_id)
check_id_change = true
old_id = get_unique_id
end
ret = super(key, value)
if check_id_change
# if we have set an attribute that results in the _id changing (unique_id),
# force changed? to return true so that the record can be saved
new_id = get_unique_id
changed_attributes["_id"] = new_id if old_id != new_id
end
ret
end
# 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.
@ -50,12 +82,17 @@ module CouchRest
# 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.
attrs = remove_protected_attributes(hash) attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs) directly_set_attributes(attrs, :dirty => true)
end end
alias :attributes= :update_attributes_without_saving alias :attributes= :update_attributes_without_saving
# 'attributes' needed for Dirty
alias :attributes :properties_with_values
def find_property(property)
property.is_a?(Property) ? property : self.class.prop_by_name[property.to_s]
end
private
# The following methods should be accessable by the Model::Base Class, but not by anything else! # The following methods should be accessable by the Model::Base Class, but not by anything else!
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)
@ -76,15 +113,16 @@ module CouchRest
end end
def find_property!(property) def find_property!(property)
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s} prop = find_property(property)
raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil? raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
prop prop
end end
# Set all the attributes and return a hash with the attributes # Set all the attributes and return a hash with the attributes
# that have not been accepted. # that have not been accepted.
def directly_set_attributes(hash) def directly_set_attributes(hash, options = {})
hash.reject do |attribute_name, attribute_value| self.disable_dirty = !options[:dirty]
ret = hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=") if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", attribute_value) self.send("#{attribute_name}=", attribute_value)
true true
@ -95,6 +133,8 @@ module CouchRest
false false
end end
end end
self.disable_dirty = false
ret
end end
def directly_set_read_only_attributes(hash) def directly_set_read_only_attributes(hash)
@ -166,13 +206,14 @@ module CouchRest
end end
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.merge(:use_dirty => use_dirty))
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
end end
properties << property properties << property
prop_by_name[property.to_s] = property
property property
end end
@ -206,7 +247,7 @@ module CouchRest
property_name = property.name property_name = property.name
class_eval <<-EOS class_eval <<-EOS
def #{property_name}=(value) def #{property_name}=(value)
write_attribute('#{property_name}', value) write_attribute_dirty('#{property_name}', value)
end end
EOS EOS

View file

@ -4,7 +4,7 @@ module CouchRest::Model
include ::CouchRest::Model::Typecast include ::CouchRest::Model::Typecast
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :use_dirty, :options
# Attribute to define. # Attribute to define.
# All Properties are assumed casted unless the type is nil. # All Properties are assumed casted unless the type is nil.
@ -38,8 +38,12 @@ module CouchRest::Model
end end
arr = value.collect { |data| cast_value(parent, data) } arr = value.collect { |data| cast_value(parent, data) }
# allow casted_by calls to be passed up chain by wrapping in CastedArray # allow casted_by calls to be passed up chain by wrapping in CastedArray
value = type_class != String ? CastedArray.new(arr, self) : arr value = (use_dirty || type_class != String) ? CastedArray.new(arr, self) : arr
value.casted_by = parent if value.respond_to?(:casted_by) value.casted_by = parent if value.respond_to?(:casted_by)
elsif (type == Object || type == Hash) && (value.class == Hash)
# allow casted_by calls to be passed up chain by wrapping in CastedHash
value = CouchRest::Model::CastedHash[value]
value.casted_by = parent
elsif !value.nil? elsif !value.nil?
value = cast_value(parent, value) value = cast_value(parent, value)
end end
@ -90,6 +94,7 @@ module CouchRest::Model
@alias = options.delete(:alias) if options[:alias] @alias = options.delete(:alias) if options[:alias]
@default = options.delete(:default) unless options[:default].nil? @default = options.delete(:default) unless options[:default].nil?
@init_method = options[:init_method] ? options.delete(:init_method) : 'new' @init_method = options[:init_method] ? options.delete(:init_method) : 'new'
@use_dirty = options.delete(:use_dirty)
@options = options @options = options
end end

View file

@ -8,6 +8,7 @@ require "active_model/serialization"
require "active_model/translation" require "active_model/translation"
require "active_model/validator" require "active_model/validator"
require "active_model/validations" require "active_model/validations"
require "active_model/dirty"
require 'active_support/core_ext' require 'active_support/core_ext'
require 'active_support/json' require 'active_support/json'
@ -26,10 +27,14 @@ require 'couchrest/model'
require 'couchrest/model/errors' require 'couchrest/model/errors'
require "couchrest/model/persistence" require "couchrest/model/persistence"
require "couchrest/model/typecast" require "couchrest/model/typecast"
require "couchrest/model/casted_by"
require "couchrest/model/dirty"
require "couchrest/model/property" require "couchrest/model/property"
require "couchrest/model/property_protection" require "couchrest/model/property_protection"
require "couchrest/model/casted_array"
require "couchrest/model/properties" require "couchrest/model/properties"
require "couchrest/model/casted_array"
require "couchrest/model/casted_hash"
require "couchrest/model/casted_model"
require "couchrest/model/validations" require "couchrest/model/validations"
require "couchrest/model/callbacks" require "couchrest/model/callbacks"
require "couchrest/model/document_queries" require "couchrest/model/document_queries"
@ -46,6 +51,7 @@ require "couchrest/model/designs/view"
# Monkey patches applied to couchrest # Monkey patches applied to couchrest
require "couchrest/model/support/couchrest_design" require "couchrest/model/support/couchrest_design"
# Core Extensions # Core Extensions
require "couchrest/model/core_extensions/hash" require "couchrest/model/core_extensions/hash"
require "couchrest/model/core_extensions/time_parsing" require "couchrest/model/core_extensions/time_parsing"
@ -53,7 +59,6 @@ require "couchrest/model/core_extensions/time_parsing"
# Base libraries # Base libraries
require "couchrest/model/casted_model" require "couchrest/model/casted_model"
require "couchrest/model/base" require "couchrest/model/base"
# Add rails support *after* everything has loaded # Add rails support *after* everything has loaded
require "couchrest/railtie" require "couchrest/railtie"

View file

@ -350,6 +350,7 @@ describe "Model Base" do
foundart.created_at.should == foundart.updated_at foundart.created_at.should == foundart.updated_at
end end
it "should set the time on update" do it "should set the time on update" do
@art.title = "new title" # only saved if @art.changed? == true
@art.save @art.save
@art.created_at.should < @art.updated_at @art.created_at.should < @art.updated_at
end end

View file

@ -160,7 +160,7 @@ describe CouchRest::Model::CastedModel do
end end
it "should cast the array properly" do it "should cast the array properly" do
@obj.keywords.should be_an_instance_of(Array) @obj.keywords.should be_kind_of(Array)
@obj.keywords.first.should == 'couch' @obj.keywords.first.should == 'couch'
end end
end end

View file

@ -0,0 +1,418 @@
require File.expand_path("../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base')
class WithCastedModelMixin < Hash
include CouchRest::Model::CastedModel
property :name
property :details, Object, :default => {}
property :casted_attribute, WithCastedModelMixin
end
class DummyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :casted_attribute, WithCastedModelMixin
property :details, Object, :default => { 'color' => 'blue' }
property :keywords, [String], :default => ['default-keyword']
property :sub_models do |child|
child.property :title
end
end
# set dirty configuration, return previous configuration setting
def set_dirty(value)
orig = nil
CouchRest::Model::Base.configure do |config|
orig = config.use_dirty
config.use_dirty = value
end
Card.instance_eval do
self.use_dirty = value
end
orig
end
describe "With use_dirty(off)" do
before(:all) do
@use_dirty_orig = set_dirty(false)
end
# turn dirty back to default
after(:all) do
set_dirty(@use_dirty_orig)
end
describe "changes" do
it "should not respond to the changes method" do
@card = Card.new
@card.first_name = "andrew"
@card.changes.should == {}
end
end
describe "changed?" do
it "should not record changes" do
@card = Card.new
@card.first_name = "andrew"
@card.changed?.should be_false
end
end
describe "save" do
it "should save unchanged records" do
@card = Card.create!(:first_name => "matt")
@card = Card.find(@card.id)
@card.database.should_receive(:save_doc).and_return({"ok" => true})
@card.save
end
end
end
describe "With use_dirty(on)" do
before(:all) do
@use_dirty_orig = set_dirty(true)
end
# turn dirty back to default
after(:all) do
set_dirty(@use_dirty_orig)
end
describe "changes" do
it "should return changes on an attribute" do
@card = Card.new(:first_name => "matt")
@card.first_name = "andrew"
@card.changes.should == { "first_name" => ["matt", "andrew"] }
end
end
describe "save" do
it "should not save unchanged records" do
card_id = Card.create!(:first_name => "matt").id
@card = Card.find(card_id)
@card.database.should_not_receive(:save_doc)
@card.save
end
it "should save changed records" do
card_id = Card.create!(:first_name => "matt").id
@card = Card.find(card_id)
@card.first_name = "andrew"
@card.database.should_receive(:save_doc).and_return({"ok" => true})
@card.save
end
end
describe "changed?" do
# match activerecord behaviour
it "should report no changes on a new object with no attributes set" do
@card = Card.new
@card.changed?.should be_false
end
it "should report no changes on a hash property with a default value" do
@obj = DummyModel.new
@obj.details.changed?.should be_false
end
=begin
# match activerecord behaviour
# not currently working - not too important
it "should report changes on a new object with attributes set" do
@card = Card.new(:first_name => "matt")
@card.changed?.should be_true
end
=end
it "should report no changes on objects fetched from the database" do
card_id = Card.create!(:first_name => "matt").id
@card = Card.find(card_id)
@card.changed?.should be_false
end
it "should report changes if the record is modified" do
@card = Card.new
@card.first_name = "andrew"
@card.changed?.should be_true
@card.first_name_changed?.should be_true
end
it "should report no changes for unmodified records" do
card_id = Card.create!(:first_name => "matt").id
@card = Card.find(card_id)
@card.first_name = "matt"
@card.changed?.should be_false
@card.first_name_changed?.should be_false
end
it "should report no changes after a new record has been saved" do
@card = Card.new(:first_name => "matt")
@card.save!
@card.changed?.should be_false
end
it "should report no changes after a record has been saved" do
card_id = Card.create!(:first_name => "matt").id
@card = Card.find(card_id)
@card.first_name = "andrew"
@card.save!
@card.changed?.should be_false
end
# test changing list properties
it "should report changes if a list property is modified" do
cat_id = Cat.create!(:name => "Felix", :toys => [{:name => "Mouse"}]).id
@cat = Cat.find(cat_id)
@cat.toys = [{:name => "Feather"}]
@cat.changed?.should be_true
end
it "should report no changes if a list property is unmodified" do
cat_id = Cat.create!(:name => "Felix", :toys => [{:name => "Mouse"}]).id
@cat = Cat.find(cat_id)
@cat.toys = [{:name => "Mouse"}] # same as original list
@cat.changed?.should be_false
end
# attachments
it "should report changes if an attachment is added" do
cat_id = Cat.create!(:name => "Felix", :toys => [{:name => "Mouse"}]).id
@file = File.open(FIXTURE_PATH + '/attachments/test.html')
@cat = Cat.find(cat_id)
@cat.create_attachment(:file => @file, :name => "my_attachment")
@cat.changed?.should be_true
end
it "should report changes if an attachment is deleted" do
@cat = Cat.create!(:name => "Felix", :toys => [{:name => "Mouse"}])
@file = File.open(FIXTURE_PATH + '/attachments/test.html')
@attachment_name = "my_attachment"
@cat.create_attachment(:file => @file, :name => @attachment_name)
@cat.save
@cat = Cat.find(@cat.id)
@cat.delete_attachment(@attachment_name)
@cat.changed?.should be_true
end
# casted models
it "should report changes to casted models" do
@cat = Cat.create!(:name => "Felix", :favorite_toy => { :name => "Mouse" })
@cat = Cat.find(@cat.id)
@cat.favorite_toy['name'] = 'Feather'
@cat.changed?.should be_true
end
# casted arrays
def test_casted_array(change_expected)
obj = DummyModel.create!
obj = DummyModel.get(obj.id)
array = obj.keywords
yield array, obj
if change_expected
obj.changed?.should be_true
else
obj.changed?.should be_false
end
end
def should_change_array
test_casted_array(true) { |a,b| yield a,b }
end
def should_not_change_array
test_casted_array(false) { |a,b| yield a,b }
end
it "should report changes if an array index is modified" do
should_change_array do |array, obj|
array[0] = "keyword"
end
end
it "should report no changes if an array index is unmodified" do
should_not_change_array do |array, obj|
array[0] = array[0]
end
end
it "should report changes if an array is appended with <<" do
should_change_array do |array, obj|
array << 'keyword'
end
end
it "should report changes if an array is popped" do
should_change_array do |array, obj|
array.pop
end
end
it "should report no changes if an empty array is popped" do
should_not_change_array do |array, obj|
array.clear
obj.save! # clears changes
array.pop
end
end
it "should report changes if an array is pushed" do
should_change_array do |array, obj|
array.push("keyword")
end
end
it "should report changes if an array is shifted" do
should_change_array do |array, obj|
array.shift
end
end
it "should report no changes if an empty array is shifted" do
should_not_change_array do |array, obj|
array.clear
obj.save! # clears changes
array.shift
end
end
it "should report changes if an array is unshifted" do
should_change_array do |array, obj|
array.unshift("keyword")
end
end
it "should report changes if an array is cleared" do
should_change_array do |array, obj|
array.clear
end
end
# Object, {} (casted hash)
def test_casted_hash(change_expected)
obj = DummyModel.create!
obj = DummyModel.get(obj.id)
hash = obj.details
yield hash, obj
if change_expected
obj.changed?.should be_true
else
obj.changed?.should be_false
end
end
def should_change_hash
test_casted_hash(true) { |a,b| yield a,b }
end
def should_not_change_hash
test_casted_hash(false) { |a,b| yield a,b }
end
it "should report changes if a hash is modified" do
should_change_hash do |hash, obj|
hash['color'] = 'orange'
end
end
it "should report no changes if a hash is unmodified" do
should_not_change_hash do |hash, obj|
hash['color'] = hash['color']
end
end
it "should report changes when deleting from a hash" do
should_change_hash do |hash, obj|
hash.delete('color')
end
end
it "should report no changes when deleting a non existent key from a hash" do
should_not_change_hash do |hash, obj|
hash.delete('non-existent-key')
end
end
it "should report changes when clearing a hash" do
should_change_hash do |hash, obj|
hash.clear
end
end
it "should report changes when merging changes to a hash" do
should_change_hash do |hash, obj|
hash.merge!('foo' => 'bar')
end
end
it "should report no changes when merging no changes to a hash" do
should_not_change_hash do |hash, obj|
hash.merge!('color' => hash['color'])
end
end
it "should report changes when replacing hash content" do
should_change_hash do |hash, obj|
hash.replace('foo' => 'bar')
end
end
it "should report no changes when replacing hash content with same content" do
should_not_change_hash do |hash, obj|
hash.replace(hash)
end
end
it "should report changes when removing records with delete_if" do
should_change_hash do |hash, obj|
hash.delete_if { true }
end
end
it "should report no changes when removing no records with delete_if" do
should_not_change_hash do |hash, obj|
hash.delete_if { false }
end
end
if {}.respond_to?(:keep_if)
it "should report changes when removing records with keep_if" do
should_change_hash do |hash, obj|
hash.keep_if { false }
end
end
it "should report no changes when removing no records with keep_if" do
should_not_change_hash do |hash, obj|
hash.keep_if { true }
end
end
end
end
end

View file

@ -131,7 +131,7 @@ class WithUniqueValidationView < CouchRest::Model::Base
attr_accessor :code attr_accessor :code
unique_id :code unique_id :code
def code def code
self["_id"] ||= @code @code
end end
property :title property :title