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:
commit
1bced3b207
18 changed files with 819 additions and 41 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,3 +7,4 @@ pkg
|
|||
couchdb.std*
|
||||
*.*~
|
||||
Gemfile.lock
|
||||
spec.out
|
||||
|
|
118
benchmarks/dirty.rb
Normal file
118
benchmarks/dirty.rb
Normal 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
|
|
@ -18,14 +18,17 @@ module CouchRest
|
|||
include CouchRest::Model::Associations
|
||||
include CouchRest::Model::Validations
|
||||
include CouchRest::Model::Designs
|
||||
include CouchRest::Model::Dirty
|
||||
include CouchRest::Model::CastedBy
|
||||
|
||||
def self.subclasses
|
||||
@subclasses ||= []
|
||||
end
|
||||
|
||||
|
||||
def self.inherited(subklass)
|
||||
super
|
||||
subklass.send(:include, CouchRest::Model::Properties)
|
||||
|
||||
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.inherited(subklass)
|
||||
super
|
||||
|
@ -36,7 +39,7 @@ module CouchRest
|
|||
EOS
|
||||
subclasses << subklass
|
||||
end
|
||||
|
||||
|
||||
# Accessors
|
||||
attr_accessor :casted_by
|
||||
|
||||
|
@ -45,7 +48,7 @@ module CouchRest
|
|||
# using the provided document hash.
|
||||
#
|
||||
# Options supported:
|
||||
#
|
||||
#
|
||||
# * :directly_set_attributes: true when data comes directly from database
|
||||
# * :database: provide an alternative database
|
||||
#
|
||||
|
@ -59,8 +62,8 @@ module CouchRest
|
|||
end
|
||||
after_initialize if respond_to?(:after_initialize)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
# Temp solution to make the view_by methods available
|
||||
def self.method_missing(m, *args, &block)
|
||||
if has_view?(m)
|
||||
|
@ -74,24 +77,17 @@ module CouchRest
|
|||
end
|
||||
super
|
||||
end
|
||||
|
||||
|
||||
### 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
|
||||
# (overrides base_doc? in casted_by.rb)
|
||||
def base_doc?
|
||||
!@casted_by
|
||||
end
|
||||
|
||||
|
||||
## Compatibility with ActiveSupport and older frameworks
|
||||
|
||||
|
||||
# Hack so that CouchRest::Document, which descends from Hash,
|
||||
# doesn't appear to Rails routing as a Hash of options
|
||||
def is_a?(klass)
|
||||
|
@ -103,14 +99,14 @@ module CouchRest
|
|||
def persisted?
|
||||
!new?
|
||||
end
|
||||
|
||||
|
||||
def to_key
|
||||
new? ? nil : [id]
|
||||
new? ? nil : [id]
|
||||
end
|
||||
|
||||
alias :to_param :id
|
||||
alias :new_record? :new?
|
||||
alias :new_document? :new?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
module CouchRest::Model
|
||||
class CastedArray < Array
|
||||
include CouchRest::Model::Dirty
|
||||
attr_accessor :casted_by
|
||||
attr_accessor :property
|
||||
|
||||
|
@ -14,15 +15,39 @@ module CouchRest::Model
|
|||
end
|
||||
|
||||
def << obj
|
||||
couchrest_parent_will_change! if use_dirty?
|
||||
super(instantiate_and_cast(obj))
|
||||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
protected
|
||||
|
|
23
lib/couchrest/model/casted_by.rb
Normal file
23
lib/couchrest/model/casted_by.rb
Normal 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
|
76
lib/couchrest/model/casted_hash.rb
Normal file
76
lib/couchrest/model/casted_hash.rb
Normal 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
|
|
@ -10,6 +10,7 @@ module CouchRest::Model
|
|||
include CouchRest::Model::PropertyProtection
|
||||
include CouchRest::Model::Associations
|
||||
include CouchRest::Model::Validations
|
||||
include CouchRest::Model::Dirty
|
||||
attr_accessor :casted_by
|
||||
end
|
||||
|
||||
|
@ -20,6 +21,7 @@ module CouchRest::Model
|
|||
end
|
||||
|
||||
def []= key, value
|
||||
couchrest_attribute_will_change!(key) if use_dirty && self[key] != value
|
||||
super(key.to_s, value)
|
||||
end
|
||||
|
||||
|
@ -64,5 +66,6 @@ module CouchRest::Model
|
|||
end
|
||||
end
|
||||
alias :attributes= :update_attributes_without_saving
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,11 +11,13 @@ module CouchRest
|
|||
add_config :model_type_key
|
||||
add_config :mass_assign_any_attribute
|
||||
add_config :auto_update_design_doc
|
||||
add_config :use_dirty
|
||||
|
||||
configure do |config|
|
||||
config.model_type_key = 'model' # was 'couchrest-type'
|
||||
config.mass_assign_any_attribute = false
|
||||
config.auto_update_design_doc = true
|
||||
config.use_dirty = true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -49,5 +51,3 @@ module CouchRest
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
|
49
lib/couchrest/model/dirty.rb
Normal file
49
lib/couchrest/model/dirty.rb
Normal 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
|
|
@ -1,6 +1,7 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module ExtendedAttachments
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# Add a file attachment to the current document. Expects
|
||||
# :file and :name to be included in the arguments.
|
||||
|
@ -35,7 +36,10 @@ module CouchRest
|
|||
# deletes a file attachment from the current doc
|
||||
def delete_attachment(attachment_name)
|
||||
return unless attachments
|
||||
attachments.delete attachment_name
|
||||
if attachments.include?(attachment_name)
|
||||
attribute_will_change!("_attachments")
|
||||
attachments.delete attachment_name
|
||||
end
|
||||
end
|
||||
|
||||
# returns true if attachment_name exists
|
||||
|
@ -66,6 +70,8 @@ module CouchRest
|
|||
def set_attachment_attr(args)
|
||||
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
|
||||
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
|
||||
|
||||
attribute_will_change!("_attachments")
|
||||
attachments[args[:name]] = {
|
||||
'content_type' => content_type,
|
||||
'data' => args[:file].read
|
||||
|
|
|
@ -12,7 +12,9 @@ module CouchRest
|
|||
_run_save_callbacks do
|
||||
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
||||
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
|
||||
|
@ -28,10 +30,13 @@ module CouchRest
|
|||
def update(options = {})
|
||||
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
|
||||
return false unless perform_validations(options)
|
||||
return true if use_dirty? && !self.changed?
|
||||
_run_update_callbacks do
|
||||
_run_save_callbacks do
|
||||
result = database.save_doc(self)
|
||||
result["ok"] == true
|
||||
ret = result["ok"] == true
|
||||
@changed_attributes.clear if ret && @changed_attributes
|
||||
ret
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -140,12 +145,18 @@ module CouchRest
|
|||
# should use the class name as part of the unique id.
|
||||
def unique_id method = nil, &block
|
||||
if method
|
||||
define_method :get_unique_id do
|
||||
self.send(method)
|
||||
end
|
||||
define_method :set_unique_id do
|
||||
self['_id'] ||= self.send(method)
|
||||
self['_id'] ||= get_unique_id
|
||||
end
|
||||
elsif block
|
||||
define_method :get_unique_id do
|
||||
block.call(self)
|
||||
end
|
||||
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?
|
||||
self['_id'] ||= uniqid
|
||||
end
|
||||
|
|
|
@ -6,7 +6,9 @@ module CouchRest
|
|||
|
||||
included do
|
||||
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.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?(:[]=))
|
||||
end
|
||||
|
||||
|
@ -43,6 +45,36 @@ module CouchRest
|
|||
self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
|
||||
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
|
||||
# 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.
|
||||
|
@ -50,12 +82,17 @@ module CouchRest
|
|||
# Remove any protected and update all the rest. Any attributes
|
||||
# which do not have a property will simply be ignored.
|
||||
attrs = remove_protected_attributes(hash)
|
||||
directly_set_attributes(attrs)
|
||||
directly_set_attributes(attrs, :dirty => true)
|
||||
end
|
||||
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!
|
||||
def apply_all_property_defaults
|
||||
return if self.respond_to?(:new?) && (new? == false)
|
||||
|
@ -76,15 +113,16 @@ module CouchRest
|
|||
end
|
||||
|
||||
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?
|
||||
prop
|
||||
end
|
||||
|
||||
# Set all the attributes and return a hash with the attributes
|
||||
# that have not been accepted.
|
||||
def directly_set_attributes(hash)
|
||||
hash.reject do |attribute_name, attribute_value|
|
||||
def directly_set_attributes(hash, options = {})
|
||||
self.disable_dirty = !options[:dirty]
|
||||
ret = hash.reject do |attribute_name, attribute_value|
|
||||
if self.respond_to?("#{attribute_name}=")
|
||||
self.send("#{attribute_name}=", attribute_value)
|
||||
true
|
||||
|
@ -95,6 +133,8 @@ module CouchRest
|
|||
false
|
||||
end
|
||||
end
|
||||
self.disable_dirty = false
|
||||
ret
|
||||
end
|
||||
|
||||
def directly_set_read_only_attributes(hash)
|
||||
|
@ -166,13 +206,14 @@ module CouchRest
|
|||
end
|
||||
type = [type] # inject as an array
|
||||
end
|
||||
property = Property.new(name, type, options)
|
||||
property = Property.new(name, type, options.merge(:use_dirty => use_dirty))
|
||||
create_property_getter(property)
|
||||
create_property_setter(property) unless property.read_only == true
|
||||
if property.type_class.respond_to?(:validates_casted_model)
|
||||
validates_casted_model property.name
|
||||
end
|
||||
properties << property
|
||||
prop_by_name[property.to_s] = property
|
||||
property
|
||||
end
|
||||
|
||||
|
@ -206,7 +247,7 @@ module CouchRest
|
|||
property_name = property.name
|
||||
class_eval <<-EOS
|
||||
def #{property_name}=(value)
|
||||
write_attribute('#{property_name}', value)
|
||||
write_attribute_dirty('#{property_name}', value)
|
||||
end
|
||||
EOS
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ module CouchRest::Model
|
|||
|
||||
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.
|
||||
# All Properties are assumed casted unless the type is nil.
|
||||
|
@ -38,8 +38,12 @@ module CouchRest::Model
|
|||
end
|
||||
arr = value.collect { |data| cast_value(parent, data) }
|
||||
# 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)
|
||||
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?
|
||||
value = cast_value(parent, value)
|
||||
end
|
||||
|
@ -90,6 +94,7 @@ module CouchRest::Model
|
|||
@alias = options.delete(:alias) if options[:alias]
|
||||
@default = options.delete(:default) unless options[:default].nil?
|
||||
@init_method = options[:init_method] ? options.delete(:init_method) : 'new'
|
||||
@use_dirty = options.delete(:use_dirty)
|
||||
@options = options
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ require "active_model/serialization"
|
|||
require "active_model/translation"
|
||||
require "active_model/validator"
|
||||
require "active_model/validations"
|
||||
require "active_model/dirty"
|
||||
|
||||
require 'active_support/core_ext'
|
||||
require 'active_support/json'
|
||||
|
@ -26,10 +27,14 @@ require 'couchrest/model'
|
|||
require 'couchrest/model/errors'
|
||||
require "couchrest/model/persistence"
|
||||
require "couchrest/model/typecast"
|
||||
require "couchrest/model/casted_by"
|
||||
require "couchrest/model/dirty"
|
||||
require "couchrest/model/property"
|
||||
require "couchrest/model/property_protection"
|
||||
require "couchrest/model/casted_array"
|
||||
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/callbacks"
|
||||
require "couchrest/model/document_queries"
|
||||
|
@ -46,6 +51,7 @@ require "couchrest/model/designs/view"
|
|||
|
||||
# Monkey patches applied to couchrest
|
||||
require "couchrest/model/support/couchrest_design"
|
||||
|
||||
# Core Extensions
|
||||
require "couchrest/model/core_extensions/hash"
|
||||
require "couchrest/model/core_extensions/time_parsing"
|
||||
|
@ -53,7 +59,6 @@ require "couchrest/model/core_extensions/time_parsing"
|
|||
# Base libraries
|
||||
require "couchrest/model/casted_model"
|
||||
require "couchrest/model/base"
|
||||
|
||||
# Add rails support *after* everything has loaded
|
||||
|
||||
require "couchrest/railtie"
|
||||
|
|
|
@ -350,6 +350,7 @@ describe "Model Base" do
|
|||
foundart.created_at.should == foundart.updated_at
|
||||
end
|
||||
it "should set the time on update" do
|
||||
@art.title = "new title" # only saved if @art.changed? == true
|
||||
@art.save
|
||||
@art.created_at.should < @art.updated_at
|
||||
end
|
||||
|
|
|
@ -160,7 +160,7 @@ describe CouchRest::Model::CastedModel do
|
|||
end
|
||||
|
||||
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'
|
||||
end
|
||||
end
|
||||
|
|
418
spec/couchrest/dirty_spec.rb
Normal file
418
spec/couchrest/dirty_spec.rb
Normal 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
|
2
spec/fixtures/base.rb
vendored
2
spec/fixtures/base.rb
vendored
|
@ -131,7 +131,7 @@ class WithUniqueValidationView < CouchRest::Model::Base
|
|||
attr_accessor :code
|
||||
unique_id :code
|
||||
def code
|
||||
self["_id"] ||= @code
|
||||
@code
|
||||
end
|
||||
property :title
|
||||
|
||||
|
|
Loading…
Reference in a new issue