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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +15 -4
  4. data/Gemfile.activesupport-4.x +4 -0
  5. data/Gemfile.activesupport-5.x +4 -0
  6. data/README.md +2 -0
  7. data/VERSION +1 -1
  8. data/couchrest_model.gemspec +3 -2
  9. data/history.md +14 -1
  10. data/lib/couchrest/model/associations.rb +3 -8
  11. data/lib/couchrest/model/base.rb +15 -7
  12. data/lib/couchrest/model/casted_array.rb +22 -34
  13. data/lib/couchrest/model/configuration.rb +2 -0
  14. data/lib/couchrest/model/design.rb +4 -3
  15. data/lib/couchrest/model/designs/view.rb +37 -32
  16. data/lib/couchrest/model/dirty.rb +93 -19
  17. data/lib/couchrest/model/embeddable.rb +2 -14
  18. data/lib/couchrest/model/extended_attachments.rb +2 -4
  19. data/lib/couchrest/model/persistence.rb +14 -17
  20. data/lib/couchrest/model/properties.rb +46 -54
  21. data/lib/couchrest/model/property.rb +0 -3
  22. data/lib/couchrest/model/proxyable.rb +20 -4
  23. data/lib/couchrest/model/validations/uniqueness.rb +4 -1
  24. data/lib/couchrest_model.rb +2 -2
  25. data/spec/fixtures/models/article.rb +1 -1
  26. data/spec/fixtures/models/card.rb +2 -1
  27. data/spec/fixtures/models/person.rb +1 -0
  28. data/spec/fixtures/models/project.rb +3 -0
  29. data/spec/unit/assocations_spec.rb +73 -73
  30. data/spec/unit/attachment_spec.rb +34 -34
  31. data/spec/unit/base_spec.rb +102 -102
  32. data/spec/unit/casted_array_spec.rb +7 -7
  33. data/spec/unit/casted_spec.rb +7 -7
  34. data/spec/unit/configuration_spec.rb +11 -11
  35. data/spec/unit/connection_spec.rb +30 -30
  36. data/spec/unit/core_extensions/{time_parsing.rb → time_parsing_spec.rb} +21 -21
  37. data/spec/unit/design_spec.rb +38 -38
  38. data/spec/unit/designs/design_mapper_spec.rb +26 -26
  39. data/spec/unit/designs/migrations_spec.rb +13 -13
  40. data/spec/unit/designs/view_spec.rb +319 -274
  41. data/spec/unit/designs_spec.rb +39 -39
  42. data/spec/unit/dirty_spec.rb +188 -103
  43. data/spec/unit/embeddable_spec.rb +119 -117
  44. data/spec/unit/inherited_spec.rb +4 -4
  45. data/spec/unit/persistence_spec.rb +122 -122
  46. data/spec/unit/properties_spec.rb +466 -16
  47. data/spec/unit/property_protection_spec.rb +32 -32
  48. data/spec/unit/property_spec.rb +45 -436
  49. data/spec/unit/proxyable_spec.rb +140 -82
  50. data/spec/unit/subclass_spec.rb +14 -14
  51. data/spec/unit/translations_spec.rb +5 -5
  52. data/spec/unit/typecast_spec.rb +131 -131
  53. data/spec/unit/utils/migrate_spec.rb +2 -2
  54. data/spec/unit/validations_spec.rb +31 -31
  55. metadata +27 -12
  56. 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
- # internal dirty setting - overrides global setting.
17
- # this is used to temporarily disable dirty tracking when setting
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
- doc = base_doc
24
- doc && !doc.disable_dirty
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
- def couchrest_attribute_will_change!(attr)
28
- return if attr.nil? || !use_dirty?
29
- attribute_will_change!(attr)
30
- couchrest_parent_will_change!
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 couchrest_parent_will_change!
34
- casted_by.couchrest_attribute_will_change!(casted_by_property.name) if casted_by_property
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(keys = {}, options = {})
30
+ def initialize(attributes = {}, options = {})
32
31
  super()
33
- prepare_all_attributes(keys, options)
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 => e
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 => e
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? && self.respond_to?(:set_unique_id)
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
- @changed_attributes.clear if ret && @changed_attributes
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 if !self.disable_dirty && !self.changed?
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
- @changed_attributes.clear if ret && @changed_attributes
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
- update_attributes_without_saving hash
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
- prepare_all_attributes(database.get(id), :directly_set_attributes => true)
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(:directly_set_attributes => true), &block)
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
- define_method :set_unique_id do
158
- self['_id'] ||= self.send(method)
159
- end
160
- elsif block
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 and update dirty status
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
- # Takes a hash as argument, and applies the values by using writer methods
49
- # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
50
- # missing. In case of error, no attributes are changed.
51
- def update_attributes_without_saving(hash)
52
- # Remove any protected and update all the rest. Any attributes
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= :update_attributes_without_saving
58
-
59
- # 'attributes' needed for Dirty
60
- alias :attributes :properties_with_values
39
+ alias :attributes :read_attributes
61
40
 
62
- def set_attributes(hash)
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
- # The following methods should be accessable by the Model::Base Class, but not by anything else!
74
- def apply_all_property_defaults
75
- return if self.respond_to?(:new?) && (new? == false)
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 prepare_all_attributes(attrs = {}, options = {})
86
- self.disable_dirty = !!options[:directly_set_attributes]
69
+ def write_attributes_for_initialization(attrs = {}, opts = {})
87
70
  apply_all_property_defaults
88
- if options[:directly_set_attributes]
89
- directly_set_read_only_attributes(attrs)
90
- directly_set_attributes(attrs, true)
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
- attrs = remove_protected_attributes(attrs)
93
- directly_set_attributes(attrs)
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
- def find_property!(property)
100
- prop = find_property(property)
101
- raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
102
- prop
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
- assign_multiparameter_attributes(multi_parameter_attributes, hash) unless multi_parameter_attributes.empty?
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