datamapper-dm-core 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -39
  5. data/Manifest.txt +67 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +16 -15
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/dm-core.gemspec +11 -15
  12. data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
  13. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  14. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  15. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  16. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  19. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  20. data/lib/dm-core/adapters.rb +135 -16
  21. data/lib/dm-core/associations/many_to_many.rb +372 -90
  22. data/lib/dm-core/associations/many_to_one.rb +220 -73
  23. data/lib/dm-core/associations/one_to_many.rb +319 -255
  24. data/lib/dm-core/associations/one_to_one.rb +66 -53
  25. data/lib/dm-core/associations/relationship.rb +560 -158
  26. data/lib/dm-core/collection.rb +1104 -381
  27. data/lib/dm-core/core_ext/kernel.rb +12 -0
  28. data/lib/dm-core/core_ext/symbol.rb +10 -0
  29. data/lib/dm-core/identity_map.rb +4 -34
  30. data/lib/dm-core/migrations.rb +1283 -0
  31. data/lib/dm-core/model/descendant_set.rb +81 -0
  32. data/lib/dm-core/model/hook.rb +45 -0
  33. data/lib/dm-core/model/is.rb +32 -0
  34. data/lib/dm-core/model/property.rb +248 -0
  35. data/lib/dm-core/model/relationship.rb +335 -0
  36. data/lib/dm-core/model/scope.rb +90 -0
  37. data/lib/dm-core/model.rb +570 -369
  38. data/lib/dm-core/property.rb +753 -280
  39. data/lib/dm-core/property_set.rb +141 -98
  40. data/lib/dm-core/query/conditions/comparison.rb +814 -0
  41. data/lib/dm-core/query/conditions/operation.rb +247 -0
  42. data/lib/dm-core/query/direction.rb +43 -0
  43. data/lib/dm-core/query/operator.rb +42 -0
  44. data/lib/dm-core/query/path.rb +102 -0
  45. data/lib/dm-core/query/sort.rb +45 -0
  46. data/lib/dm-core/query.rb +974 -492
  47. data/lib/dm-core/repository.rb +147 -107
  48. data/lib/dm-core/resource.rb +644 -429
  49. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  50. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  51. data/lib/dm-core/support/chainable.rb +20 -0
  52. data/lib/dm-core/support/deprecate.rb +12 -0
  53. data/lib/dm-core/support/equalizer.rb +23 -0
  54. data/lib/dm-core/support/logger.rb +13 -0
  55. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  56. data/lib/dm-core/transaction.rb +333 -92
  57. data/lib/dm-core/type.rb +98 -60
  58. data/lib/dm-core/types/boolean.rb +1 -1
  59. data/lib/dm-core/types/discriminator.rb +34 -20
  60. data/lib/dm-core/types/object.rb +7 -4
  61. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  62. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  63. data/lib/dm-core/types/serial.rb +3 -3
  64. data/lib/dm-core/types/text.rb +3 -4
  65. data/lib/dm-core/version.rb +1 -1
  66. data/lib/dm-core.rb +106 -110
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/model/relationship_spec.rb +924 -0
  80. data/spec/public/model_spec.rb +159 -0
  81. data/spec/public/property_spec.rb +829 -0
  82. data/spec/public/resource_spec.rb +71 -0
  83. data/spec/public/sel_spec.rb +44 -0
  84. data/spec/public/setup_spec.rb +145 -0
  85. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  86. data/spec/public/shared/collection_shared_spec.rb +1723 -0
  87. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  88. data/spec/public/shared/resource_shared_spec.rb +924 -0
  89. data/spec/public/shared/sel_shared_spec.rb +112 -0
  90. data/spec/public/transaction_spec.rb +129 -0
  91. data/spec/public/types/discriminator_spec.rb +130 -0
  92. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  93. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  94. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  95. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  96. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  97. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  99. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  100. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  101. data/spec/semipublic/associations_spec.rb +177 -0
  102. data/spec/semipublic/collection_spec.rb +142 -0
  103. data/spec/semipublic/property_spec.rb +61 -0
  104. data/spec/semipublic/query/conditions_spec.rb +528 -0
  105. data/spec/semipublic/query/path_spec.rb +443 -0
  106. data/spec/semipublic/query_spec.rb +2626 -0
  107. data/spec/semipublic/resource_spec.rb +47 -0
  108. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  109. data/spec/spec.opts +3 -1
  110. data/spec/spec_helper.rb +80 -57
  111. data/tasks/ci.rb +19 -31
  112. data/tasks/dm.rb +43 -48
  113. data/tasks/doc.rb +8 -11
  114. data/tasks/gemspec.rb +5 -5
  115. data/tasks/hoe.rb +15 -16
  116. data/tasks/install.rb +8 -10
  117. metadata +72 -93
  118. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  119. data/lib/dm-core/associations.rb +0 -207
  120. data/lib/dm-core/auto_migrations.rb +0 -105
  121. data/lib/dm-core/dependency_queue.rb +0 -32
  122. data/lib/dm-core/hook.rb +0 -11
  123. data/lib/dm-core/is.rb +0 -16
  124. data/lib/dm-core/logger.rb +0 -232
  125. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  126. data/lib/dm-core/migrator.rb +0 -29
  127. data/lib/dm-core/scope.rb +0 -58
  128. data/lib/dm-core/support/array.rb +0 -13
  129. data/lib/dm-core/support/assertions.rb +0 -8
  130. data/lib/dm-core/support/errors.rb +0 -23
  131. data/lib/dm-core/support/kernel.rb +0 -11
  132. data/lib/dm-core/support/symbol.rb +0 -41
  133. data/lib/dm-core/support.rb +0 -7
  134. data/lib/dm-core/type_map.rb +0 -80
  135. data/lib/dm-core/types.rb +0 -19
  136. data/script/all +0 -4
  137. data/spec/integration/association_spec.rb +0 -1382
  138. data/spec/integration/association_through_spec.rb +0 -203
  139. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  140. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  141. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  142. data/spec/integration/auto_migrations_spec.rb +0 -413
  143. data/spec/integration/collection_spec.rb +0 -1073
  144. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  145. data/spec/integration/dependency_queue_spec.rb +0 -46
  146. data/spec/integration/model_spec.rb +0 -197
  147. data/spec/integration/mysql_adapter_spec.rb +0 -85
  148. data/spec/integration/postgres_adapter_spec.rb +0 -731
  149. data/spec/integration/property_spec.rb +0 -253
  150. data/spec/integration/query_spec.rb +0 -514
  151. data/spec/integration/repository_spec.rb +0 -61
  152. data/spec/integration/resource_spec.rb +0 -513
  153. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  154. data/spec/integration/sti_spec.rb +0 -273
  155. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  156. data/spec/integration/transaction_spec.rb +0 -75
  157. data/spec/integration/type_spec.rb +0 -275
  158. data/spec/lib/logging_helper.rb +0 -18
  159. data/spec/lib/mock_adapter.rb +0 -27
  160. data/spec/lib/model_loader.rb +0 -100
  161. data/spec/lib/publicize_methods.rb +0 -28
  162. data/spec/models/content.rb +0 -16
  163. data/spec/models/vehicles.rb +0 -34
  164. data/spec/models/zoo.rb +0 -48
  165. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  166. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  167. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  168. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  169. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  170. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  171. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  172. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  173. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  174. data/spec/unit/associations/relationship_spec.rb +0 -71
  175. data/spec/unit/associations_spec.rb +0 -242
  176. data/spec/unit/auto_migrations_spec.rb +0 -111
  177. data/spec/unit/collection_spec.rb +0 -182
  178. data/spec/unit/data_mapper_spec.rb +0 -35
  179. data/spec/unit/identity_map_spec.rb +0 -126
  180. data/spec/unit/is_spec.rb +0 -80
  181. data/spec/unit/migrator_spec.rb +0 -33
  182. data/spec/unit/model_spec.rb +0 -321
  183. data/spec/unit/naming_conventions_spec.rb +0 -36
  184. data/spec/unit/property_set_spec.rb +0 -90
  185. data/spec/unit/property_spec.rb +0 -753
  186. data/spec/unit/query_spec.rb +0 -571
  187. data/spec/unit/repository_spec.rb +0 -93
  188. data/spec/unit/resource_spec.rb +0 -649
  189. data/spec/unit/scope_spec.rb +0 -142
  190. data/spec/unit/transaction_spec.rb +0 -493
  191. data/spec/unit/type_map_spec.rb +0 -114
  192. data/spec/unit/type_spec.rb +0 -119
@@ -1,94 +1,161 @@
1
- require 'set'
2
-
3
1
  module DataMapper
4
2
  module Resource
5
- include Assertions
3
+ include Extlib::Assertions
4
+ extend Chainable
5
+ extend Deprecate
6
6
 
7
- ##
8
- #
9
- # Appends a module for inclusion into the model class after
10
- # DataMapper::Resource.
11
- #
12
- # This is a useful way to extend DataMapper::Resource while still retaining
13
- # a self.included method.
14
- #
15
- # @param [Module] inclusion the module that is to be appended to the module
16
- # after DataMapper::Resource
17
- #
18
- # @return [TrueClass, FalseClass] whether or not the inclusions have been
19
- # successfully appended to the list
20
- # @return <TrueClass, FalseClass>
21
- #-
22
- # @api public
7
+ deprecate :new_record?, :new?
8
+
9
+ # @deprecated
23
10
  def self.append_inclusions(*inclusions)
24
- extra_inclusions.concat inclusions
25
- true
11
+ warn "DataMapper::Resource.append_inclusions is deprecated, use DataMapper::Model.append_inclusions instead (#{caller[0]})"
12
+ Model.append_inclusions(*inclusions)
26
13
  end
27
14
 
15
+ # @deprecated
28
16
  def self.extra_inclusions
29
- @extra_inclusions ||= []
17
+ warn "DataMapper::Resource.extra_inclusions is deprecated, use DataMapper::Model.extra_inclusions instead (#{caller[0]})"
18
+ Model.extra_inclusions
19
+ end
20
+
21
+ # @deprecated
22
+ def self.descendants
23
+ warn "DataMapper::Resource.descendants is deprecated, use DataMapper::Model.descendants instead (#{caller[0]})"
24
+ Model.descendants
25
+ end
26
+
27
+ # Deprecated API for updating attributes and saving Resource
28
+ #
29
+ # @see #update
30
+ #
31
+ # @deprecated
32
+ def update_attributes(attributes = {}, *allowed)
33
+ assert_update_clean_only(:update_attributes)
34
+
35
+ warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller[0]})"
36
+
37
+ if allowed.any?
38
+ warn "specifying allowed in #{model}#update_attributes is deprecated, " \
39
+ "use Hash#only to filter the attributes in the caller (#{caller[0]})"
40
+ attributes = attributes.only(*allowed)
41
+ end
42
+
43
+ update(attributes)
30
44
  end
31
45
 
32
- # When Resource is included in a class this method makes sure
33
- # it gets all the methods
46
+ # Makes sure a class gets all the methods when it includes Resource
34
47
  #
35
- # -
36
48
  # @api private
37
49
  def self.included(model)
38
50
  model.extend Model
39
- model.extend ClassMethods if defined?(ClassMethods)
40
- model.const_set('Resource', self) unless model.const_defined?('Resource')
41
- extra_inclusions.each { |inclusion| model.send(:include, inclusion) }
42
- descendants << model
43
- class << model
44
- @_valid_model = false
45
- attr_reader :_valid_model
46
- end
47
51
  end
48
52
 
49
- # Return all classes that include the DataMapper::Resource module
53
+ # Collection this resource associated with.
54
+ # Used by SEL.
50
55
  #
51
- # ==== Returns
52
- # Set:: a set containing the including classes
56
+ # @api private
57
+ attr_writer :collection
58
+
59
+ # TODO: document
60
+ # @api public
61
+ alias_method :model, :class
62
+
63
+ # Repository this resource belongs to in the context of this collection
64
+ # or of the resource's class.
53
65
  #
54
- # ==== Example
66
+ # @return [Repository]
67
+ # the respository this resource belongs to, in the context of
68
+ # a collection OR in the instance's Model's context
55
69
  #
56
- # Class Foo
57
- # include DataMapper::Resource
58
- # end
70
+ # @api semipublic
71
+ def repository
72
+ # only set @repository explicitly when persisted
73
+ defined?(@repository) ? @repository : model.repository
74
+ end
75
+
76
+ # Retrieve the key(s) for this resource.
59
77
  #
60
- # DataMapper::Resource.descendants.to_a.first == Foo
78
+ # This always returns the persisted key value,
79
+ # even if the key is changed and not yet persisted.
80
+ # This is done so all relations still work.
61
81
  #
62
- # -
63
- # @api semipublic
64
- def self.descendants
65
- @descendants ||= Set.new
82
+ # @return [Array(Key)]
83
+ # the key(s) identifying this resource
84
+ #
85
+ # @api public
86
+ def key
87
+ return @key if defined?(@key)
88
+
89
+ key = model.key(repository_name).map do |property|
90
+ original_attributes[property] || (property.loaded?(self) ? property.get!(self) : nil)
91
+ end
92
+
93
+ return unless key.all?
94
+
95
+ # memoize the key if the Resource is not frozen
96
+ @key = key unless frozen?
97
+
98
+ key
66
99
  end
67
100
 
68
- # +---------------
69
- # Instance methods
101
+ # Checks if this Resource instance is new
102
+ #
103
+ # @return [Boolean]
104
+ # true if the resource is new and not saved
105
+ #
106
+ # @api public
107
+ def new?
108
+ !saved?
109
+ end
70
110
 
71
- attr_writer :collection
111
+ # Checks if this Resource instance is saved
112
+ #
113
+ # @return [Boolean]
114
+ # true if the resource has been saved
115
+ #
116
+ # @api public
117
+ def saved?
118
+ @saved == true
119
+ end
72
120
 
73
- alias model class
121
+ # Checks if the resource has no changes to save
122
+ #
123
+ # @return [Boolean]
124
+ # true if the resource may not be persisted
125
+ #
126
+ # @api public
127
+ def clean?
128
+ !dirty?
129
+ end
74
130
 
75
- # returns the value of the attribute. Do not read from instance variables directly,
76
- # but use this method. This method handels the lazy loading the attribute and returning
77
- # of defaults if nessesary.
131
+ # Checks if the resource has unsaved changes
78
132
  #
79
- # ==== Parameters
80
- # name<Symbol>:: name attribute to lookup
133
+ # @return [Boolean]
134
+ # true if resource may be persisted
81
135
  #
82
- # ==== Returns
83
- # <Types>:: the value stored at that given attribute, nil if none, and default if necessary
136
+ # @api public
137
+ def dirty?
138
+ if original_attributes.any?
139
+ true
140
+ elsif new?
141
+ model.serial || properties.any? { |property| property.default? }
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ # Returns the value of the attribute.
84
148
  #
85
- # ==== Example
149
+ # Do not read from instance variables directly, but use this method.
150
+ # This method handles lazy loading the attribute and returning of
151
+ # defaults if nessesary.
86
152
  #
87
- # Class Foo
153
+ # @example
154
+ # class Foo
88
155
  # include DataMapper::Resource
89
156
  #
90
157
  # property :first_name, String
91
- # property :last_name, String
158
+ # property :last_name, String
92
159
  #
93
160
  # def full_name
94
161
  # "#{attribute_get(:first_name)} #{attribute_get(:last_name)}"
@@ -100,32 +167,32 @@ module DataMapper
100
167
  # end
101
168
  # end
102
169
  #
103
- # -
104
- # @api semipublic
170
+ # @param [Symbol] name
171
+ # name of attribute to retrieve
172
+ #
173
+ # @return [Object]
174
+ # the value stored at that given attribute
175
+ # (nil if none, and default if necessary)
176
+ #
177
+ # @api public
105
178
  def attribute_get(name)
106
179
  properties[name].get(self)
107
180
  end
108
181
 
109
- # sets the value of the attribute and marks the attribute as dirty
182
+ alias [] attribute_get
183
+
184
+ # Sets the value of the attribute and marks the attribute as dirty
110
185
  # if it has been changed so that it may be saved. Do not set from
111
186
  # instance variables directly, but use this method. This method
112
- # handels the lazy loading the property and returning of defaults
187
+ # handles the lazy loading the property and returning of defaults
113
188
  # if nessesary.
114
189
  #
115
- # ==== Parameters
116
- # name<Symbol>:: name attribute to set
117
- # value<Type>:: value to store at that location
118
- #
119
- # ==== Returns
120
- # <Types>:: the value stored at that given attribute, nil if none, and default if necessary
121
- #
122
- # ==== Example
123
- #
124
- # Class Foo
190
+ # @example
191
+ # class Foo
125
192
  # include DataMapper::Resource
126
193
  #
127
194
  # property :first_name, String
128
- # property :last_name, String
195
+ # property :last_name, String
129
196
  #
130
197
  # def full_name(name)
131
198
  # name = name.split(' ')
@@ -141,531 +208,679 @@ module DataMapper
141
208
  # end
142
209
  # end
143
210
  #
144
- # -
145
- # @api semipublic
211
+ # @param [Symbol] name
212
+ # name of attribute to set
213
+ # @param [Object] value
214
+ # value to store
215
+ #
216
+ # @return [Object]
217
+ # the value stored at that given attribute, nil if none,
218
+ # and default if necessary
219
+ #
220
+ # @api public
146
221
  def attribute_set(name, value)
147
222
  properties[name].set(self, value)
148
223
  end
149
224
 
150
- # Compares if its the same object or if attributes are equal
225
+ alias []= attribute_set
226
+
227
+ # Gets all the attributes of the Resource instance
151
228
  #
152
- # The comparaison is
153
- # * false if object not from same repository
154
- # * false if object has no all same properties
229
+ # @param [Symbol] key_on
230
+ # Use this attribute of the Property as keys.
231
+ # defaults to :name. :field is useful for adapters
232
+ # :property or nil use the actual Property object.
155
233
  #
234
+ # @return [Hash]
235
+ # All the attributes
156
236
  #
157
- # ==== Parameters
158
- # other<Object>:: Object to compare to
237
+ # @api public
238
+ def attributes(key_on = :name)
239
+ attributes = {}
240
+ properties.each do |property|
241
+ if model.public_method_defined?(name = property.name)
242
+ key = case key_on
243
+ when :name then name
244
+ when :field then property.field
245
+ else property
246
+ end
247
+
248
+ attributes[key] = send(name)
249
+ end
250
+ end
251
+ attributes
252
+ end
253
+
254
+ # Assign values to multiple attributes in one call (mass assignment)
255
+ #
256
+ # @param [Hash] attributes
257
+ # names and values of attributes to assign
159
258
  #
160
- # ==== Returns
161
- # <True>:: the outcome of the comparison as a boolean
259
+ # @return [Hash]
260
+ # names and values of attributes assigned
162
261
  #
163
- # -
164
262
  # @api public
165
- def eql?(other)
166
- return true if equal?(other)
167
-
168
- # two instances for different models cannot be equivalent
169
- return false unless other.kind_of?(model)
170
-
171
- # two instances with different keys cannot be equivalent
172
- return false if key != other.key
173
-
174
- # neither object has changed since loaded, so they are equivalent
175
- return true if repository == other.repository && !dirty? && !other.dirty?
176
-
177
- # get all the loaded and non-loaded properties that are not keys,
178
- # since the key comparison was performed earlier
179
- loaded, not_loaded = properties.select { |p| !p.key? }.partition do |property|
180
- attribute_loaded?(property.name) && other.attribute_loaded?(property.name)
263
+ def attributes=(attributes)
264
+ attributes.each do |name, value|
265
+ case name
266
+ when String, Symbol
267
+ if model.public_method_defined?(setter = "#{name}=")
268
+ send(setter, value)
269
+ else
270
+ raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
271
+ end
272
+ when Associations::Relationship, Property
273
+ name.set(self, value)
274
+ end
181
275
  end
182
-
183
- # check all loaded properties, and then all unloaded properties
184
- (loaded + not_loaded).all? { |p| p.get(self) == p.get(other) }
185
276
  end
186
277
 
187
- alias == eql?
188
-
189
- # Computes a hash for the resource
278
+ # Reloads association and all child association
190
279
  #
191
- # ==== Returns
192
- # <Integer>:: the hash value of the resource
280
+ # @return [Resource]
281
+ # the receiver, the current Resource instance
193
282
  #
194
- # -
195
283
  # @api public
196
- def hash
197
- model.hash + key.hash
284
+ def reload
285
+ if saved?
286
+ eager_load(loaded_properties)
287
+ child_relationships.each { |relationship| relationship.get!(self).reload }
288
+ end
289
+
290
+ self
198
291
  end
199
292
 
200
- # Inspection of the class name and the attributes
201
- #
202
- # ==== Returns
203
- # <String>:: with the class name, attributes with their values
293
+ # Updates attributes and saves this Resource instance
204
294
  #
205
- # ==== Example
295
+ # @param [Hash] attributes
296
+ # attributes to be updated
206
297
  #
207
- # >> Foo.new
208
- # => #<Foo name=nil updated_at=nil created_at=nil id=nil>
298
+ # @return [Boolean]
299
+ # true if resource and storage state match
209
300
  #
210
- # -
211
301
  # @api public
212
- def inspect
213
- attrs = []
214
-
215
- properties.each do |property|
216
- value = if !attribute_loaded?(property.name) && !new_record?
217
- '<not loaded>'
218
- else
219
- send(property.getter).inspect
220
- end
221
-
222
- attrs << "#{property.name}=#{value}"
302
+ chainable do
303
+ def update(attributes = {})
304
+ assert_update_clean_only(:update)
305
+ self.attributes = attributes
306
+ save
223
307
  end
308
+ end
224
309
 
225
- "#<#{model.name} #{attrs * ' '}>"
310
+ # Updates attributes and saves this Resource instance, bypassing hooks
311
+ #
312
+ # @param [Hash] attributes
313
+ # attributes to be updated
314
+ #
315
+ # @return [Boolean]
316
+ # true if resource and storage state match
317
+ #
318
+ # @api public
319
+ def update!(attributes = {})
320
+ assert_update_clean_only(:update!)
321
+ self.attributes = attributes
322
+ save!
226
323
  end
227
324
 
228
- # TODO docs
229
- def pretty_print(pp)
230
- pp.group(1, "#<#{model.name}", ">") do
231
- pp.breakable
232
- pp.seplist(attributes.to_a) do |k_v|
233
- pp.text k_v[0].to_s
234
- pp.text " = "
235
- pp.pp k_v[1]
236
- end
325
+ # Save the instance and loaded, dirty associations to the data-store
326
+ #
327
+ # @return [Boolean]
328
+ # true if Resource instance and all associations were saved
329
+ #
330
+ # @api public
331
+ chainable do
332
+ def save
333
+ save_parents && save_self && save_children
237
334
  end
238
335
  end
239
336
 
240
- ##
337
+ # Save the instance and loaded, dirty associations to the data-store, bypassing hooks
241
338
  #
242
- # ==== Returns
243
- # <Repository>:: the respository this resource belongs to in the context of a collection OR in the class's context
339
+ # @return [Boolean]
340
+ # true if Resource instance and all associations were saved
244
341
  #
245
342
  # @api public
246
- def repository
247
- @repository || model.repository
343
+ def save!
344
+ save_parents(false) && save_self(false) && save_children(false)
248
345
  end
249
346
 
250
- # default id method to return the resource id when there is a
251
- # single key, and the model was defined with a primary key named
252
- # something other than id
347
+ # Destroy the instance, remove it from the repository
253
348
  #
254
- # ==== Returns
255
- # <Array[Key], Key> key or keys
349
+ # @return [Boolean]
350
+ # true if resource was destroyed
256
351
  #
257
- # --
258
352
  # @api public
259
- def id
260
- key = self.key
261
- key.first if key.size == 1
353
+ chainable do
354
+ def destroy
355
+ destroy!
356
+ end
262
357
  end
263
358
 
264
- def key
265
- key_properties.map do |property|
266
- original_values[property.name] || property.get!(self)
359
+ # Destroy the instance, remove it from the repository, bypassing hooks
360
+ #
361
+ # @return [Boolean]
362
+ # true if resource was destroyed
363
+ #
364
+ # @api public
365
+ def destroy!
366
+ if saved? && repository.delete(Collection.new(query, [ self ])) == 1
367
+ @collection.delete(self) if @collection
368
+ reset
369
+ freeze
370
+ true
371
+ else
372
+ false
267
373
  end
268
374
  end
269
375
 
270
- def readonly!
271
- @readonly = true
376
+ # Compares another Resource for equality
377
+ #
378
+ # Resource is equal to +other+ if they are the same object (identity)
379
+ # or if they are both of the *same model* and all of their attributes
380
+ # are equivalent
381
+ #
382
+ # @param [Resource] other
383
+ # the other Resource to compare with
384
+ #
385
+ # @return [Boolean]
386
+ # true if they are equal, false if not
387
+ #
388
+ # @api public
389
+ def eql?(other)
390
+ return true if equal?(other)
391
+ instance_of?(other.class) && cmp?(other, :eql?)
272
392
  end
273
393
 
274
- def readonly?
275
- @readonly == true
394
+ # Compares another Resource for equivalency
395
+ #
396
+ # Resource is equal to +other+ if they are the same object (identity)
397
+ # or if they are both of the *same base model* and all of their attributes
398
+ # are equivalent
399
+ #
400
+ # @param [Resource] other
401
+ # the other Resource to compare with
402
+ #
403
+ # @return [Boolean]
404
+ # true if they are equivalent, false if not
405
+ #
406
+ # @api public
407
+ def ==(other)
408
+ return true if equal?(other)
409
+ other.respond_to?(:model) &&
410
+ model.base_model.equal?(other.model.base_model) &&
411
+ cmp?(other, :==)
276
412
  end
277
413
 
278
- # save the instance to the data-store
414
+ # Compares two Resources to allow them to be sorted
279
415
  #
280
- # ==== Returns
281
- # <True, False>:: results of the save
416
+ # @param [Resource] other
417
+ # The other Resource to compare with
282
418
  #
283
- # @see DataMapper::Repository#save
419
+ # @return [Integer]
420
+ # Return 0 if Resources should be sorted as the same, -1 if the
421
+ # other Resource should be after self, and 1 if the other Resource
422
+ # should be before self
284
423
  #
285
- # --
286
- # #public
287
- def save(context = :default)
288
- # Takes a context, but does nothing with it. This is to maintain the
289
- # same API through out all of dm-more. dm-validations requires a
290
- # context to be passed
291
-
292
- associations_saved = false
293
- child_associations.each { |a| associations_saved |= a.save }
294
-
295
- saved = new_record? ? create : update
296
-
297
- if saved
298
- original_values.clear
424
+ # @api public
425
+ def <=>(other)
426
+ unless other.kind_of?(model.base_model)
427
+ raise ArgumentError, "Cannot compare a #{other.model} instance with a #{model} instance"
299
428
  end
429
+ cmp = 0
430
+ model.default_order(repository_name).each do |direction|
431
+ cmp = direction.get(self) <=> direction.get(other)
432
+ break if cmp != 0
433
+ end
434
+ cmp
435
+ end
300
436
 
301
- parent_associations.each { |a| associations_saved |= a.save }
302
-
303
- # We should return true if the model (or any of its associations)
304
- # were saved.
305
- (saved | associations_saved) == true
437
+ # Returns hash value of the object.
438
+ # Two objects with the same hash value assumed equal (using eql? method)
439
+ #
440
+ # DataMapper resources are equal when their models have the same hash
441
+ # and they have the same set of properties
442
+ #
443
+ # When used as key in a Hash or Hash subclass, objects are compared
444
+ # by eql? and thus hash value has direct effect on lookup
445
+ #
446
+ # @api private
447
+ def hash
448
+ key.hash
306
449
  end
307
450
 
308
- # destroy the instance, remove it from the repository
451
+ # Get a Human-readable representation of this Resource instance
452
+ #
453
+ # Foo.new #=> #<Foo name=nil updated_at=nil created_at=nil id=nil>
309
454
  #
310
- # ==== Returns
311
- # <True, False>:: results of the destruction
455
+ # @return [String]
456
+ # Human-readable representation of this Resource instance
312
457
  #
313
- # --
314
458
  # @api public
315
- def destroy
316
- return false if new_record?
317
- return false unless repository.delete(to_query)
318
-
319
- @new_record = true
320
- repository.identity_map(model).delete(key)
321
- original_values.clear
459
+ def inspect
460
+ # TODO: display relationship values
461
+ attrs = properties.map do |property|
462
+ value = if new? || property.loaded?(self)
463
+ property.get!(self).inspect
464
+ else
465
+ '<not loaded>'
466
+ end
322
467
 
323
- properties.each do |property|
324
- # We'll set the original value to nil as if we had a new record
325
- original_values[property.name] = nil if attribute_loaded?(property.name)
468
+ "#{property.instance_variable_name}=#{value}"
326
469
  end
327
470
 
328
- true
471
+ "#<#{model.name} #{attrs.join(' ')}>"
329
472
  end
330
473
 
331
- # Checks if the attribute has been loaded
332
- #
333
- # ==== Example
334
- #
335
- # class Foo
336
- # include DataMapper::Resource
337
- # property :name, String
338
- # property :description, Text, :lazy => false
339
- # end
474
+ # Hash of original values of attributes that have unsaved changes
340
475
  #
341
- # Foo.new.attribute_loaded?(:description) # will return false
476
+ # @return [Hash]
477
+ # original values of attributes that have unsaved changes
342
478
  #
343
- # --
344
- # @api public
345
- def attribute_loaded?(name)
346
- instance_variable_defined?(properties[name].instance_variable_name)
479
+ # @api semipublic
480
+ def original_attributes
481
+ @original_attributes ||= {}
347
482
  end
348
483
 
349
- # fetches all the names of the attributes that have been loaded,
350
- # even if they are lazy but have been called
351
- #
352
- # ==== Returns
353
- # Array[<Symbol>]:: names of attributes that have been loaded
354
- #
355
- # ==== Example
484
+ # Checks if an attribute has been loaded from the repository
356
485
  #
486
+ # @example
357
487
  # class Foo
358
488
  # include DataMapper::Resource
359
- # property :name, String
360
- # property :description, Text, :lazy => false
489
+ #
490
+ # property :name, String
491
+ # property :description, Text, :lazy => false
361
492
  # end
362
493
  #
363
- # Foo.new.loaded_attributes # returns [:name]
494
+ # Foo.new.attribute_loaded?(:description) #=> false
364
495
  #
365
- # --
366
- # @api public
367
- def loaded_attributes
368
- properties.map{|p| p.name if attribute_loaded?(p.name)}.compact
496
+ # @return [Boolean]
497
+ # true if ivar +name+ has been loaded
498
+ #
499
+ # @return [Boolean]
500
+ # true if ivar +name+ has been loaded
501
+ #
502
+ # @api private
503
+ def attribute_loaded?(name)
504
+ properties[name].loaded?(self)
369
505
  end
370
506
 
371
- # set of original values of properties
507
+ # Checks if an attribute has unsaved changes
372
508
  #
373
- # ==== Returns
374
- # Hash:: original values of properties
509
+ # @param [Symbol] name
510
+ # name of attribute to check for unsaved changes
375
511
  #
376
- # --
377
- # @api public
378
- def original_values
379
- @original_values ||= {}
512
+ # @return [Boolean]
513
+ # true if attribute has unsaved changes
514
+ #
515
+ # @api semipublic
516
+ def attribute_dirty?(name)
517
+ dirty_attributes.key?(properties[name])
380
518
  end
381
519
 
382
- # Hash of attributes that have been marked dirty
520
+ # Hash of attributes that have unsaved changes
383
521
  #
384
- # ==== Returns
385
- # Hash:: attributes that have been marked dirty
522
+ # @return [Hash]
523
+ # attributes that have unsaved changes
386
524
  #
387
- # --
388
- # @api private
525
+ # @api semipublic
389
526
  def dirty_attributes
390
527
  dirty_attributes = {}
391
- properties = self.properties
392
-
393
- original_values.each do |name, old_value|
394
- property = properties[name]
395
- new_value = property.get!(self)
396
-
397
- dirty = case property.track
398
- when :hash then old_value != new_value.hash
399
- else
400
- property.value(old_value) != property.value(new_value)
401
- end
402
528
 
403
- if dirty
404
- property.hash
405
- dirty_attributes[property] = property.value(new_value)
406
- end
529
+ original_attributes.each_key do |property|
530
+ dirty_attributes[property] = property.value(property.get!(self))
407
531
  end
408
532
 
409
533
  dirty_attributes
410
534
  end
411
535
 
412
- # Checks if the class is dirty
536
+ # Saves the resource
413
537
  #
414
- # ==== Returns
415
- # True:: returns if class is dirty
538
+ # @return [Boolean]
539
+ # true if the resource was successfully saved
416
540
  #
417
- # --
418
- # @api public
419
- def dirty?
420
- dirty_attributes.any?
541
+ # @api semipublic
542
+ def save_self(safe = true)
543
+ if safe
544
+ new? ? create_hook : update_hook
545
+ else
546
+ new? ? _create : _update
547
+ end
421
548
  end
422
549
 
423
- # Checks if the attribute is dirty
550
+ # Saves the parent resources
424
551
  #
425
- # ==== Parameters
426
- # name<Symbol>:: name of attribute
552
+ # @return [Boolean]
553
+ # true if the parents were successfully saved
427
554
  #
428
- # ==== Returns
429
- # True:: returns if attribute is dirty
555
+ # @api private
556
+ def save_parents(safe = true)
557
+ parent_relationships.all? do |relationship|
558
+ parent = relationship.get!(self)
559
+ if parent.dirty? ? parent.save_parents(safe) && parent.save_self(safe) : parent.saved?
560
+ relationship.set(self, parent) # set the FK values
561
+ end
562
+ end
563
+ end
564
+
565
+ # Saves the children resources
430
566
  #
431
- # --
432
- # @api public
433
- def attribute_dirty?(name)
434
- dirty_attributes.has_key?(properties[name])
567
+ # @return [Boolean]
568
+ # true if the children were successfully saved
569
+ #
570
+ # @api private
571
+ def save_children(safe = true)
572
+ child_relationships.all? do |relationship|
573
+ association = relationship.get!(self)
574
+ safe ? association.save : association.save!
575
+ end
435
576
  end
436
577
 
578
+ # Reset the Resource to a similar state as a new record:
579
+ # removes it from identity map and clears original property
580
+ # values (thus making all properties non dirty)
581
+ #
582
+ # @api private
583
+ def reset
584
+ @saved = false
585
+ identity_map.delete(key)
586
+ original_attributes.clear
587
+ self
588
+ end
589
+
590
+ # Gets a Collection with the current Resource instance as its only member
591
+ #
592
+ # @return [Collection, FalseClass]
593
+ # nil if this is a new record,
594
+ # otherwise a Collection with self as its only member
595
+ #
596
+ # @api private
437
597
  def collection
438
- @collection ||= if query = to_query
439
- Collection.new(query) { |c| c << self }
440
- end
598
+ return @collection if @collection || new? || frozen?
599
+ @collection = Collection.new(query, [ self ])
441
600
  end
442
601
 
443
- # Reload association and all child association
602
+ protected
603
+
604
+ # Method for hooking callbacks on resource creation
444
605
  #
445
- # ==== Returns
446
- # self:: returns the class itself
606
+ # @return [Boolean]
607
+ # true if the create was successful, false if not
447
608
  #
448
- # --
449
- # @api public
450
- def reload
451
- unless new_record?
452
- reload_attributes(*loaded_attributes)
453
- (parent_associations + child_associations).each { |association| association.reload }
454
- end
609
+ # @api private
610
+ def create_hook
611
+ _create
612
+ end
455
613
 
456
- self
614
+ # Method for hooking callbacks on resource updates
615
+ #
616
+ # @return [Boolean]
617
+ # true if the update was successful, false if not
618
+ #
619
+ # @api private
620
+ def update_hook
621
+ _update
457
622
  end
458
623
 
459
- # Reload specific attributes
624
+ private
625
+
626
+ # Initialize a new instance of this Resource using the provided values
460
627
  #
461
- # ==== Parameters
462
- # *attributes<Array[<Symbol>]>:: name of attribute
628
+ # @param [Hash] attributes
629
+ # attribute values to use for the new instance
463
630
  #
464
- # ==== Returns
465
- # self:: returns the class itself
631
+ # @return [Hash]
632
+ # attribute values used in the new instance
466
633
  #
467
- # --
468
634
  # @api public
469
- def reload_attributes(*attributes)
470
- unless attributes.empty? || new_record?
471
- collection.reload(:fields => attributes)
472
- end
635
+ def initialize(attributes = {}, &block) # :nodoc:
636
+ self.attributes = attributes
637
+ end
473
638
 
474
- self
639
+ # Returns name of the repository this object
640
+ # was loaded from
641
+ #
642
+ # @return [String]
643
+ # name of the repository this object was loaded from
644
+ #
645
+ # @api private
646
+ def repository_name
647
+ repository.name
475
648
  end
476
649
 
477
- # Checks if the model has been saved
650
+ # Gets this instance's Model's properties
478
651
  #
479
- # ==== Returns
480
- # True:: status if the model is new
652
+ # @return [Array(Property)]
653
+ # List of this Resource's Model's properties
481
654
  #
482
- # --
483
- # @api public
484
- def new_record?
485
- !defined?(@new_record) || @new_record
655
+ # @api private
656
+ def properties
657
+ model.properties(repository_name)
486
658
  end
487
659
 
488
- # all the attributes of the model
660
+ # Gets this instance's Model's relationships
489
661
  #
490
- # ==== Returns
491
- # Hash[<Symbol>]:: All the (non)-lazy attributes
662
+ # @return [Array(Associations::Relationship)]
663
+ # List of this instance's Model's Relationships
492
664
  #
493
- # --
494
- # @api public
495
- def attributes
496
- properties.map do |p|
497
- [p.name, send(p.getter)] if p.reader_visibility == :public
498
- end.compact.to_hash
665
+ # @api private
666
+ def relationships
667
+ model.relationships(repository_name)
499
668
  end
500
669
 
501
- # Mass assign of attributes
670
+ # Returns the identity map for the model from the repository
502
671
  #
503
- # ==== Parameters
504
- # value_hash <Hash[<Symbol>]>::
672
+ # @return [IdentityMap]
673
+ # identity map of repository this object was loaded from
505
674
  #
506
- # --
507
- # @api public
508
- def attributes=(values_hash)
509
- values_hash.each do |name, value|
510
- name = name.to_s.sub(/\?\z/, '')
675
+ # @api semipublic
676
+ def identity_map
677
+ repository.identity_map(model)
678
+ end
511
679
 
512
- if self.class.public_method_defined?(setter = "#{name}=")
513
- send(setter, value)
514
- else
515
- raise ArgumentError, "The property '#{name}' is not a public property."
516
- end
517
- end
680
+ # Fetches all the names of the attributes that have been loaded,
681
+ # even if they are lazy but have been called
682
+ #
683
+ # @return [Array<Property>]
684
+ # names of attributes that have been loaded
685
+ #
686
+ # @api private
687
+ def loaded_properties
688
+ properties.select { |property| property.loaded?(self) }
518
689
  end
519
690
 
520
- # Updates attributes and saves model
691
+ # Lazy loads attributes not yet loaded
521
692
  #
522
- # ==== Parameters
523
- # attributes<Hash> Attributes to be updated
524
- # keys<Symbol, String, Array> keys of Hash to update (others won't be updated)
693
+ # @param [Array<Property>] fields
694
+ # the properties to reload
525
695
  #
526
- # ==== Returns
527
- # <TrueClass, FalseClass> if model got saved or not
696
+ # @return [self]
528
697
  #
529
- #-
530
- # @api public
531
- def update_attributes(hash, *update_only)
532
- unless hash.is_a?(Hash)
533
- raise ArgumentError, "Expecting the first parameter of " +
534
- "update_attributes to be a hash; got #{hash.inspect}"
698
+ # @api private
699
+ def lazy_load(fields)
700
+ eager_load(fields - loaded_properties)
701
+ end
702
+
703
+ # Reloads specified attributes
704
+ #
705
+ # @param [Array<Property>] fields
706
+ # the properties to reload
707
+ #
708
+ # @return [Resource]
709
+ # the receiver, the current Resource instance
710
+ #
711
+ # @api private
712
+ def eager_load(fields)
713
+ unless fields.empty? || new?
714
+ collection.reload(:fields => fields)
535
715
  end
536
- loop_thru = update_only.empty? ? hash.keys : update_only
537
- loop_thru.each { |attr| send("#{attr}=", hash[attr]) }
538
- save
716
+
717
+ self
539
718
  end
540
719
 
541
- # TODO: add docs
542
- def to_query(query = {})
543
- model.to_query(repository, key, query) unless new_record?
720
+ # Gets a Query that will return this Resource instance
721
+ #
722
+ # @return [Query]
723
+ # Query that will retrieve this Resource instance
724
+ #
725
+ # @api private
726
+ def query
727
+ Query.new(repository, model, model.key_conditions(repository, key))
544
728
  end
545
729
 
546
- # TODO: add docs
730
+ # TODO: document
547
731
  # @api private
548
- def _dump(*)
549
- ivars = {}
732
+ def parent_relationships
733
+ parent_relationships = []
550
734
 
551
- # dump all the loaded properties
552
- properties.each do |property|
553
- next unless attribute_loaded?(property.name)
554
- ivars[property.instance_variable_name] = property.get!(self)
555
- end
735
+ relationships.each_value do |relationship|
736
+ next unless relationship.respond_to?(:resource_for) && relationship.loaded?(self)
737
+ next unless relationship.get(self)
556
738
 
557
- # dump ivars used internally
558
- %w[ @new_record @original_values @readonly @repository ].each do |name|
559
- ivars[name] = instance_variable_get(name)
739
+ parent_relationships << relationship
560
740
  end
561
741
 
562
- Marshal.dump(ivars)
742
+ parent_relationships
563
743
  end
564
744
 
565
- protected
745
+ # Returns loaded child relationships
746
+ #
747
+ # @return [Array<Associations::OneToMany::Relationship>]
748
+ # array of child relationships for which this resource is parent and is loaded
749
+ #
750
+ # @api private
751
+ def child_relationships
752
+ child_relationships = []
566
753
 
567
- def properties
568
- model.properties(repository.name)
569
- end
754
+ relationships.each_value do |relationship|
755
+ next unless relationship.respond_to?(:collection_for) && relationship.loaded?(self)
570
756
 
571
- def key_properties
572
- model.key(repository.name)
573
- end
757
+ association = relationship.get!(self)
758
+ next unless association.loaded? || association.head.any? || association.tail.any?
574
759
 
575
- def relationships
576
- model.relationships(repository.name)
760
+ child_relationships << relationship
761
+ end
762
+
763
+ many_to_many, other = child_relationships.partition do |relationship|
764
+ relationship.kind_of?(Associations::ManyToMany::Relationship)
765
+ end
766
+
767
+ many_to_many + other
577
768
  end
578
769
 
770
+ # Creates the resource with default values
771
+ #
772
+ # If resource is not dirty or a new (not yet saved),
773
+ # this method returns false
774
+ #
775
+ # On successful save identity map of the repository is
776
+ # updated
777
+ #
579
778
  # Needs to be a protected method so that it is hookable
580
- def create
779
+ #
780
+ # The primary purpose of this method is to allow before :create
781
+ # hooks to fire at a point just before/after resource creation
782
+ #
783
+ # @return [Boolean]
784
+ # true if the receiver was successfully created
785
+ #
786
+ # @api private
787
+ def _create
581
788
  # Can't create a resource that is not dirty and doesn't have serial keys
582
- return false if new_record? && !dirty? && !model.key.any? { |p| p.serial? }
789
+ return false if new? && !dirty?
790
+
583
791
  # set defaults for new resource
584
792
  properties.each do |property|
585
- next if attribute_loaded?(property.name)
586
- property.set(self, property.default_for(self))
793
+ unless property.serial? || property.loaded?(self)
794
+ property.set(self, property.default_for(self))
795
+ end
587
796
  end
588
797
 
589
- return false unless repository.create([ self ]) == 1
798
+ repository.create([ self ])
590
799
 
591
800
  @repository = repository
592
- @new_record = false
801
+ @saved = true
593
802
 
594
- repository.identity_map(model).set(key, self)
803
+ original_attributes.clear
804
+
805
+ identity_map[key] = self
595
806
 
596
807
  true
597
808
  end
598
809
 
599
- # Needs to be a protected method so that it is hookable
600
- def update
810
+ # Updates resource state
811
+ #
812
+ # The primary purpose of this method is to allow before :update
813
+ # hooks to fire at a point just before/after resource update whether
814
+ # it is the result of Resource#save, or using Resource#update
815
+ #
816
+ # @return [Boolean]
817
+ # true if the receiver was successfully created
818
+ #
819
+ # @api private
820
+ def _update
601
821
  dirty_attributes = self.dirty_attributes
602
- return true if dirty_attributes.empty?
603
- repository.update(dirty_attributes, to_query) == 1
604
- end
605
822
 
606
- private
607
-
608
- def initialize(attributes = {}) # :nodoc:
609
- assert_valid_model
610
- self.attributes = attributes
611
- end
612
-
613
- def assert_valid_model # :nodoc:
614
- return if self.class._valid_model
615
- properties = self.properties
823
+ if dirty_attributes.empty?
824
+ true
825
+ elsif dirty_attributes.any? { |property, value| !property.nullable? && value.nil? }
826
+ false
827
+ else
828
+ # remove from the identity map
829
+ identity_map.delete(key)
616
830
 
617
- if properties.empty? && relationships.empty?
618
- raise IncompleteResourceError, "#{model.name} must have at least one property or relationship to be initialized."
619
- end
831
+ return false unless repository.update(dirty_attributes, Collection.new(query, [ self ])) == 1
620
832
 
621
- if properties.key.empty?
622
- raise IncompleteResourceError, "#{model.name} must have a key."
623
- end
833
+ # remove the cached key in case it is updated
834
+ remove_instance_variable(:@key)
624
835
 
625
- self.class.instance_variable_set("@_valid_model", true)
626
- end
836
+ original_attributes.clear
627
837
 
628
- # TODO document
629
- # @api semipublic
630
- def attribute_get!(name)
631
- properties[name].get!(self)
632
- end
838
+ identity_map[key] = self
633
839
 
634
- # TODO document
635
- # @api semipublic
636
- def attribute_set!(name, value)
637
- properties[name].set!(self, value)
840
+ true
841
+ end
638
842
  end
639
843
 
640
- def lazy_load(name)
641
- reload_attributes(*properties.lazy_load_context(name) - loaded_attributes)
642
- end
844
+ # Return true if +other+'s is equivalent or equal to +self+'s
845
+ #
846
+ # @param [Resource] other
847
+ # The Resource whose attributes are to be compared with +self+'s
848
+ # @param [Symbol] operator
849
+ # The comparison operator to use to compare the attributes
850
+ #
851
+ # @return [Boolean]
852
+ # The result of the comparison of +other+'s attributes with +self+'s
853
+ #
854
+ # @api private
855
+ def cmp?(other, operator)
856
+ return false unless key.send(operator, other.key)
857
+ return true if repository.send(operator, other.repository) && !dirty? && !other.dirty?
643
858
 
644
- def child_associations
645
- @child_associations ||= []
646
- end
859
+ # get all the loaded and non-loaded properties that are not keys,
860
+ # since the key comparison was performed earlier
861
+ loaded, not_loaded = properties.select { |property| !property.key? }.partition do |property|
862
+ property.loaded?(self) && property.loaded?(other)
863
+ end
647
864
 
648
- def parent_associations
649
- @parent_associations ||= []
865
+ # check all loaded properties, and then all unloaded properties
866
+ (loaded + not_loaded).all? { |property| property.get(self).send(operator, property.get(other)) }
650
867
  end
651
868
 
652
- # TODO: move to dm-more/dm-transactions
653
- module Transaction
654
- # Produce a new Transaction for the class of this Resource
655
- #
656
- # ==== Returns
657
- # <DataMapper::Adapters::Transaction>::
658
- # a new DataMapper::Adapters::Transaction with all DataMapper::Repositories
659
- # of the class of this DataMapper::Resource added.
660
- #-
661
- # @api public
662
- #
663
- # TODO: move to dm-more/dm-transactions
664
- def transaction
665
- model.transaction { |*block_args| yield(*block_args) }
869
+ # Raises an exception if #update is performed on a dirty resource
870
+ #
871
+ # @param [Symbol] method
872
+ # the name of the method to use in the exception
873
+ #
874
+ # @return [undefined]
875
+ #
876
+ # @raise [UpdateConflictError]
877
+ # raise if the resource is dirty
878
+ #
879
+ # @api private
880
+ def assert_update_clean_only(method)
881
+ if original_attributes.any?
882
+ raise UpdateConflictError, "#{model}##{method} cannot be called on a dirty resource"
666
883
  end
667
- end # module Transaction
668
-
669
- include Transaction
884
+ end
670
885
  end # module Resource
671
886
  end # module DataMapper