couchrest_model 2.1.0.rc1 → 2.2.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|