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.
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