couchrest_model 2.1.0.rc1 → 2.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +15 -4
- data/Gemfile.activesupport-4.x +4 -0
- data/Gemfile.activesupport-5.x +4 -0
- data/README.md +2 -0
- data/VERSION +1 -1
- data/couchrest_model.gemspec +3 -2
- data/history.md +14 -1
- data/lib/couchrest/model/associations.rb +3 -8
- data/lib/couchrest/model/base.rb +15 -7
- data/lib/couchrest/model/casted_array.rb +22 -34
- data/lib/couchrest/model/configuration.rb +2 -0
- data/lib/couchrest/model/design.rb +4 -3
- data/lib/couchrest/model/designs/view.rb +37 -32
- data/lib/couchrest/model/dirty.rb +93 -19
- data/lib/couchrest/model/embeddable.rb +2 -14
- data/lib/couchrest/model/extended_attachments.rb +2 -4
- data/lib/couchrest/model/persistence.rb +14 -17
- data/lib/couchrest/model/properties.rb +46 -54
- data/lib/couchrest/model/property.rb +0 -3
- data/lib/couchrest/model/proxyable.rb +20 -4
- data/lib/couchrest/model/validations/uniqueness.rb +4 -1
- data/lib/couchrest_model.rb +2 -2
- data/spec/fixtures/models/article.rb +1 -1
- data/spec/fixtures/models/card.rb +2 -1
- data/spec/fixtures/models/person.rb +1 -0
- data/spec/fixtures/models/project.rb +3 -0
- data/spec/unit/assocations_spec.rb +73 -73
- data/spec/unit/attachment_spec.rb +34 -34
- data/spec/unit/base_spec.rb +102 -102
- data/spec/unit/casted_array_spec.rb +7 -7
- data/spec/unit/casted_spec.rb +7 -7
- data/spec/unit/configuration_spec.rb +11 -11
- data/spec/unit/connection_spec.rb +30 -30
- data/spec/unit/core_extensions/{time_parsing.rb → time_parsing_spec.rb} +21 -21
- data/spec/unit/design_spec.rb +38 -38
- data/spec/unit/designs/design_mapper_spec.rb +26 -26
- data/spec/unit/designs/migrations_spec.rb +13 -13
- data/spec/unit/designs/view_spec.rb +319 -274
- data/spec/unit/designs_spec.rb +39 -39
- data/spec/unit/dirty_spec.rb +188 -103
- data/spec/unit/embeddable_spec.rb +119 -117
- data/spec/unit/inherited_spec.rb +4 -4
- data/spec/unit/persistence_spec.rb +122 -122
- data/spec/unit/properties_spec.rb +466 -16
- data/spec/unit/property_protection_spec.rb +32 -32
- data/spec/unit/property_spec.rb +45 -436
- data/spec/unit/proxyable_spec.rb +140 -82
- data/spec/unit/subclass_spec.rb +14 -14
- data/spec/unit/translations_spec.rb +5 -5
- data/spec/unit/typecast_spec.rb +131 -131
- data/spec/unit/utils/migrate_spec.rb +2 -2
- data/spec/unit/validations_spec.rb +31 -31
- metadata +27 -12
- data/lib/couchrest/model/casted_hash.rb +0 -84
@@ -1,39 +1,113 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
I18n.load_path << File.join(
|
4
|
-
File.dirname(__FILE__), "validations", "locale", "en.yml"
|
5
|
-
)
|
6
|
-
|
7
1
|
module CouchRest
|
8
2
|
module Model
|
9
3
|
|
10
4
|
# This applies to both Model::Base and Model::CastedModel
|
11
5
|
module Dirty
|
12
6
|
extend ActiveSupport::Concern
|
13
|
-
include ActiveModel::Dirty
|
14
7
|
|
15
8
|
included do
|
16
|
-
#
|
17
|
-
|
18
|
-
# attributes directly, for performance reasons.
|
19
|
-
self.send(:attr_accessor, :disable_dirty)
|
9
|
+
# The original attributes data hash, used for comparing changes.
|
10
|
+
self.send(:attr_reader, :original_change_data)
|
20
11
|
end
|
21
12
|
|
22
13
|
def use_dirty?
|
23
|
-
|
24
|
-
|
14
|
+
# Use the configuration option.
|
15
|
+
!disable_dirty_tracking
|
16
|
+
end
|
17
|
+
|
18
|
+
# Provide an array of changes according to the hashdiff gem of the raw
|
19
|
+
# json hash data.
|
20
|
+
# If dirty tracking is disabled, this will always return nil.
|
21
|
+
def changes
|
22
|
+
if original_change_data.nil?
|
23
|
+
nil
|
24
|
+
else
|
25
|
+
HashDiff.diff(original_change_data, current_change_data)
|
26
|
+
end
|
25
27
|
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
# Has this model changed? If dirty tracking is disabled, this method
|
30
|
+
# will always return true.
|
31
|
+
def changed?
|
32
|
+
diff = changes
|
33
|
+
diff.nil? || !diff.empty?
|
31
34
|
end
|
32
35
|
|
33
|
-
def
|
34
|
-
|
36
|
+
def clear_changes_information
|
37
|
+
if use_dirty?
|
38
|
+
# Recursively clear all change information
|
39
|
+
self.class.properties.each do |property|
|
40
|
+
val = read_attribute(property)
|
41
|
+
if val.respond_to?(:clear_changes_information)
|
42
|
+
val.clear_changes_information
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@original_change_data = current_change_data
|
46
|
+
else
|
47
|
+
@original_change_data = nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def current_change_data
|
54
|
+
as_couch_json.as_json
|
35
55
|
end
|
36
56
|
|
57
|
+
module ClassMethods
|
58
|
+
|
59
|
+
def create_dirty_property_methods(property)
|
60
|
+
create_dirty_property_change_method(property)
|
61
|
+
create_dirty_property_changed_method(property)
|
62
|
+
create_dirty_property_was_method(property)
|
63
|
+
end
|
64
|
+
|
65
|
+
# For #property_change.
|
66
|
+
# Tries to be a bit more efficient by directly comparing the properties
|
67
|
+
# current value with that stored in the original change data. This also
|
68
|
+
# maintains compatibility with ActiveModel change results.
|
69
|
+
def create_dirty_property_change_method(property)
|
70
|
+
define_method("#{property.name}_change") do
|
71
|
+
val = read_attribute(property.name)
|
72
|
+
if val.respond_to?(:changes)
|
73
|
+
val.changes
|
74
|
+
else
|
75
|
+
if original_change_data.nil?
|
76
|
+
nil
|
77
|
+
else
|
78
|
+
orig = original_change_data[property.name]
|
79
|
+
cur = val.as_json
|
80
|
+
if orig != cur
|
81
|
+
[orig, cur]
|
82
|
+
else
|
83
|
+
[]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# For #property_was value.
|
91
|
+
# Uses the original raw value, if available.
|
92
|
+
def create_dirty_property_was_method(property)
|
93
|
+
define_method("#{property.name}_was") do
|
94
|
+
if original_change_data.nil?
|
95
|
+
nil
|
96
|
+
else
|
97
|
+
original_change_data[property.name]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# For #property_changed?
|
103
|
+
def create_dirty_property_changed_method(property)
|
104
|
+
define_method("#{property.name}_changed?") do
|
105
|
+
changes = send("#{property.name}_change")
|
106
|
+
changes.nil? || !changes.empty?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
37
111
|
end
|
38
112
|
end
|
39
113
|
end
|
@@ -21,16 +21,15 @@ module CouchRest::Model
|
|
21
21
|
def base_doc?
|
22
22
|
false # Can never be base doc!
|
23
23
|
end
|
24
|
-
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
28
27
|
# Initialize a new Casted Model. Accepts the same
|
29
28
|
# options as CouchRest::Model::Base for preparing and initializing
|
30
29
|
# attributes.
|
31
|
-
def initialize(
|
30
|
+
def initialize(attributes = {}, options = {})
|
32
31
|
super()
|
33
|
-
|
32
|
+
write_attributes_for_initialization(attributes, options)
|
34
33
|
run_callbacks(:initialize) { self }
|
35
34
|
end
|
36
35
|
|
@@ -54,17 +53,6 @@ module CouchRest::Model
|
|
54
53
|
alias :to_key :id
|
55
54
|
alias :to_param :id
|
56
55
|
|
57
|
-
# Sets the attributes from a hash
|
58
|
-
def update_attributes_without_saving(hash)
|
59
|
-
hash.each do |k, v|
|
60
|
-
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
|
61
|
-
end
|
62
|
-
hash.each do |k, v|
|
63
|
-
self.send("#{k}=",v)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
alias :attributes= :update_attributes_without_saving
|
67
|
-
|
68
56
|
end # End Embeddable
|
69
57
|
|
70
58
|
# Provide backwards compatability with previous versions (pre 1.1.0)
|
@@ -9,7 +9,7 @@ module CouchRest
|
|
9
9
|
raise ArgumentError unless args[:file] && args[:name]
|
10
10
|
return if has_attachment?(args[:name])
|
11
11
|
set_attachment_attr(args)
|
12
|
-
rescue ArgumentError
|
12
|
+
rescue ArgumentError
|
13
13
|
raise ArgumentError, 'You must specify :file and :name'
|
14
14
|
end
|
15
15
|
|
@@ -29,7 +29,7 @@ module CouchRest
|
|
29
29
|
return unless has_attachment?(args[:name])
|
30
30
|
delete_attachment(args[:name])
|
31
31
|
set_attachment_attr(args)
|
32
|
-
rescue ArgumentError
|
32
|
+
rescue ArgumentError
|
33
33
|
raise ArgumentError, 'You must specify :file and :name'
|
34
34
|
end
|
35
35
|
|
@@ -37,7 +37,6 @@ module CouchRest
|
|
37
37
|
def delete_attachment(attachment_name)
|
38
38
|
return unless attachments
|
39
39
|
if attachments.include?(attachment_name)
|
40
|
-
attribute_will_change!("_attachments")
|
41
40
|
attachments.delete attachment_name
|
42
41
|
end
|
43
42
|
end
|
@@ -71,7 +70,6 @@ module CouchRest
|
|
71
70
|
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
|
72
71
|
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
|
73
72
|
|
74
|
-
attribute_will_change!("_attachments")
|
75
73
|
attachments[args[:name]] = {
|
76
74
|
'content_type' => content_type,
|
77
75
|
'data' => args[:file].read
|
@@ -10,10 +10,10 @@ module CouchRest
|
|
10
10
|
return false unless perform_validations(options)
|
11
11
|
run_callbacks :create do
|
12
12
|
run_callbacks :save do
|
13
|
-
set_unique_id if new? &&
|
13
|
+
set_unique_id if new? && respond_to?(:set_unique_id)
|
14
14
|
result = database.save_doc(self)
|
15
15
|
ret = (result["ok"] == true) ? self : false
|
16
|
-
|
16
|
+
clear_changes_information if ret
|
17
17
|
ret
|
18
18
|
end
|
19
19
|
end
|
@@ -31,12 +31,12 @@ module CouchRest
|
|
31
31
|
raise "Cannot save a destroyed document!" if destroyed?
|
32
32
|
raise "Calling #{self.class.name}#update on document that has not been created!" if new?
|
33
33
|
return false unless perform_validations(options)
|
34
|
-
return true
|
34
|
+
return true unless changed?
|
35
35
|
run_callbacks :update do
|
36
36
|
run_callbacks :save do
|
37
37
|
result = database.save_doc(self)
|
38
38
|
ret = result["ok"] == true
|
39
|
-
|
39
|
+
clear_changes_information if ret
|
40
40
|
ret
|
41
41
|
end
|
42
42
|
end
|
@@ -83,7 +83,7 @@ module CouchRest
|
|
83
83
|
# doc.save
|
84
84
|
#
|
85
85
|
def update_attributes(hash)
|
86
|
-
|
86
|
+
write_attributes(hash)
|
87
87
|
save
|
88
88
|
end
|
89
89
|
|
@@ -92,7 +92,9 @@ module CouchRest
|
|
92
92
|
#
|
93
93
|
# Returns self.
|
94
94
|
def reload
|
95
|
-
|
95
|
+
write_attributes_for_initialization(
|
96
|
+
database.get(id), :write_all_attributes => true
|
97
|
+
)
|
96
98
|
self
|
97
99
|
end
|
98
100
|
|
@@ -121,7 +123,7 @@ module CouchRest
|
|
121
123
|
def build_from_database(doc = {}, options = {}, &block)
|
122
124
|
src = doc[model_type_key]
|
123
125
|
base = (src.blank? || src == model_type_value) ? self : src.constantize
|
124
|
-
base.new(doc, options.merge(:
|
126
|
+
base.new(doc, options.merge(:write_all_attributes => true), &block)
|
125
127
|
end
|
126
128
|
|
127
129
|
# Defines an instance and save it directly to the database
|
@@ -153,16 +155,11 @@ module CouchRest
|
|
153
155
|
# database, so if you'd like to scope uniqueness to this class, you
|
154
156
|
# should use the class name as part of the unique id.
|
155
157
|
def unique_id(method = nil, &block)
|
156
|
-
if method
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
define_method :set_unique_id do
|
162
|
-
uniqid = block.call(self)
|
163
|
-
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
164
|
-
self['_id'] ||= uniqid
|
165
|
-
end
|
158
|
+
return if method.nil? && !block
|
159
|
+
define_method :set_unique_id do
|
160
|
+
uniqid = method.nil? ? block.call(self) : send(method)
|
161
|
+
raise ArgumentError, "unique_id cannot be nil nor empty" if uniqid.blank?
|
162
|
+
self['_id'] ||= uniqid
|
166
163
|
end
|
167
164
|
end
|
168
165
|
|
@@ -9,7 +9,6 @@ module CouchRest
|
|
9
9
|
class_attribute(:properties_by_name) unless self.respond_to?(:properties_by_name)
|
10
10
|
self.properties ||= []
|
11
11
|
self.properties_by_name ||= {}
|
12
|
-
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?(:[]=))
|
13
12
|
end
|
14
13
|
|
15
14
|
# Provide an attribute hash ready to be sent to CouchDB but with
|
@@ -18,50 +17,42 @@ module CouchRest
|
|
18
17
|
super.delete_if{|k,v| v.nil?}
|
19
18
|
end
|
20
19
|
|
21
|
-
# Returns the Class properties with their values
|
22
|
-
#
|
23
|
-
# ==== Returns
|
24
|
-
# Array:: the list of properties with their values
|
25
|
-
def properties_with_values
|
26
|
-
props = {}
|
27
|
-
properties.each { |property| props[property.name] = read_attribute(property.name) }
|
28
|
-
props
|
29
|
-
end
|
30
|
-
|
31
20
|
# Read the casted value of an attribute defined with a property.
|
32
|
-
#
|
33
|
-
# ==== Returns
|
34
|
-
# Object:: the casted attibutes value.
|
35
21
|
def read_attribute(property)
|
36
22
|
self[find_property!(property).to_s]
|
37
23
|
end
|
38
24
|
|
39
25
|
# Store a casted value in the current instance of an attribute defined
|
40
|
-
# with a property
|
26
|
+
# with a property.
|
41
27
|
def write_attribute(property, value)
|
42
28
|
prop = find_property!(property)
|
43
29
|
value = prop.cast(self, value)
|
44
|
-
couchrest_attribute_will_change!(prop.name) if use_dirty? && self[prop.name] != value
|
45
30
|
self[prop.name] = value
|
46
31
|
end
|
47
32
|
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
def
|
52
|
-
|
53
|
-
# which do not have a property will simply be ignored.
|
54
|
-
attrs = remove_protected_attributes(hash)
|
55
|
-
directly_set_attributes(attrs)
|
33
|
+
# Returns a hash of this object's attributes with a defined property.
|
34
|
+
# This is effectively an accessor to the underlying CouchRest
|
35
|
+
# attributes hash.
|
36
|
+
def read_attributes
|
37
|
+
@_attributes
|
56
38
|
end
|
57
|
-
alias :attributes
|
58
|
-
|
59
|
-
# 'attributes' needed for Dirty
|
60
|
-
alias :attributes :properties_with_values
|
39
|
+
alias :attributes :read_attributes
|
61
40
|
|
62
|
-
|
41
|
+
# Takes a hash as argument, and applies the values by using writer
|
42
|
+
# methods respecting protected properties.
|
43
|
+
def write_attributes(hash)
|
63
44
|
attrs = remove_protected_attributes(hash)
|
64
45
|
directly_set_attributes(attrs)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
alias :attributes= :write_attributes
|
49
|
+
|
50
|
+
# Takes the provided attribute hash and sets all properties, assuming
|
51
|
+
# that the data is from a trusted source, such as the database.
|
52
|
+
def write_all_attributes(attrs = {})
|
53
|
+
directly_set_read_only_attributes(attrs)
|
54
|
+
directly_set_attributes(attrs, true)
|
55
|
+
self
|
65
56
|
end
|
66
57
|
|
67
58
|
protected
|
@@ -70,36 +61,31 @@ module CouchRest
|
|
70
61
|
property.is_a?(Property) ? property : self.class.properties_by_name[property.to_s]
|
71
62
|
end
|
72
63
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
# TODO: cache the default object
|
77
|
-
# Never mark default options as dirty!
|
78
|
-
dirty, self.disable_dirty = self.disable_dirty, true
|
79
|
-
self.class.properties.each do |property|
|
80
|
-
write_attribute(property, property.default_value)
|
81
|
-
end
|
82
|
-
self.disable_dirty = dirty
|
64
|
+
def find_property!(property)
|
65
|
+
find_property(property) or
|
66
|
+
raise ArgumentError, "Missing property definition for #{property.to_s}"
|
83
67
|
end
|
84
68
|
|
85
|
-
def
|
86
|
-
self.disable_dirty = !!options[:directly_set_attributes]
|
69
|
+
def write_attributes_for_initialization(attrs = {}, opts = {})
|
87
70
|
apply_all_property_defaults
|
88
|
-
if
|
89
|
-
|
90
|
-
|
71
|
+
if opts[:write_all_attributes]
|
72
|
+
# Assume coming from a database, so we clear change information after
|
73
|
+
write_all_attributes(attrs)
|
74
|
+
clear_changes_information
|
91
75
|
else
|
92
|
-
|
93
|
-
|
76
|
+
# Not from a persisted source, clear the change data in advance and do
|
77
|
+
# not set protected or read-only attributes.
|
78
|
+
clear_changes_information
|
79
|
+
write_attributes(attrs)
|
94
80
|
end
|
95
|
-
self.disable_dirty = false
|
96
|
-
self
|
97
81
|
end
|
98
82
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
83
|
+
# Apply each property's default value to the attributes. This should
|
84
|
+
# only ever be called on initialization.
|
85
|
+
def apply_all_property_defaults
|
86
|
+
self.class.properties.each do |property|
|
87
|
+
write_attribute(property, property.default_value)
|
88
|
+
end
|
103
89
|
end
|
104
90
|
|
105
91
|
# Set all the attributes and return a hash with the attributes
|
@@ -116,12 +102,15 @@ module CouchRest
|
|
116
102
|
elsif self.respond_to?("#{key}=")
|
117
103
|
self.send("#{key}=", value)
|
118
104
|
elsif mass_assign || mass_assign_any_attribute
|
119
|
-
couchrest_attribute_will_change!(key) if use_dirty? && self[key] != value
|
120
105
|
self[key] = value
|
121
106
|
end
|
122
107
|
end
|
123
108
|
|
124
|
-
|
109
|
+
# Handle attributes provided in an embedded object format, such
|
110
|
+
# as a web-form.
|
111
|
+
unless multi_parameter_attributes.empty?
|
112
|
+
assign_multiparameter_attributes(multi_parameter_attributes, hash)
|
113
|
+
end
|
125
114
|
end
|
126
115
|
|
127
116
|
def directly_set_read_only_attributes(hash)
|
@@ -214,6 +203,9 @@ module CouchRest
|
|
214
203
|
validates_casted_model property.name
|
215
204
|
end
|
216
205
|
|
206
|
+
# Dirty!
|
207
|
+
create_dirty_property_methods(property)
|
208
|
+
|
217
209
|
properties << property
|
218
210
|
properties_by_name[property.to_s] = property
|
219
211
|
property
|
@@ -39,9 +39,6 @@ module CouchRest::Model
|
|
39
39
|
arr.reject!{ |data| data.nil? } unless allow_blank
|
40
40
|
# allow casted_by calls to be passed up chain by wrapping in CastedArray
|
41
41
|
CastedArray.new(arr, self, parent)
|
42
|
-
elsif (type == Object || type == Hash) && (value.is_a?(Hash))
|
43
|
-
# allow casted_by calls to be passed up chain by wrapping in CastedHash
|
44
|
-
CastedHash[value, self, parent]
|
45
42
|
elsif !value.nil?
|
46
43
|
cast_value(parent, value)
|
47
44
|
end
|