dm-core 0.10.2 → 1.0.0.rc1

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 (183) hide show
  1. data/.gitignore +10 -1
  2. data/Gemfile +143 -0
  3. data/Rakefile +9 -5
  4. data/VERSION +1 -1
  5. data/dm-core.gemspec +160 -57
  6. data/lib/dm-core.rb +131 -56
  7. data/lib/dm-core/adapters.rb +98 -14
  8. data/lib/dm-core/adapters/abstract_adapter.rb +24 -4
  9. data/lib/dm-core/adapters/in_memory_adapter.rb +7 -2
  10. data/lib/dm-core/associations/many_to_many.rb +19 -30
  11. data/lib/dm-core/associations/many_to_one.rb +58 -42
  12. data/lib/dm-core/associations/one_to_many.rb +33 -23
  13. data/lib/dm-core/associations/one_to_one.rb +27 -11
  14. data/lib/dm-core/associations/relationship.rb +4 -4
  15. data/lib/dm-core/collection.rb +23 -16
  16. data/lib/dm-core/core_ext/array.rb +36 -0
  17. data/lib/dm-core/core_ext/hash.rb +30 -0
  18. data/lib/dm-core/core_ext/module.rb +46 -0
  19. data/lib/dm-core/core_ext/object.rb +31 -0
  20. data/lib/dm-core/core_ext/pathname.rb +20 -0
  21. data/lib/dm-core/core_ext/string.rb +22 -0
  22. data/lib/dm-core/core_ext/try_dup.rb +44 -0
  23. data/lib/dm-core/model.rb +88 -27
  24. data/lib/dm-core/model/hook.rb +75 -18
  25. data/lib/dm-core/model/property.rb +50 -9
  26. data/lib/dm-core/model/relationship.rb +31 -31
  27. data/lib/dm-core/model/scope.rb +3 -3
  28. data/lib/dm-core/property.rb +196 -516
  29. data/lib/dm-core/property/binary.rb +7 -0
  30. data/lib/dm-core/property/boolean.rb +35 -0
  31. data/lib/dm-core/property/class.rb +24 -0
  32. data/lib/dm-core/property/date.rb +47 -0
  33. data/lib/dm-core/property/date_time.rb +48 -0
  34. data/lib/dm-core/property/decimal.rb +43 -0
  35. data/lib/dm-core/property/discriminator.rb +48 -0
  36. data/lib/dm-core/property/float.rb +24 -0
  37. data/lib/dm-core/property/integer.rb +32 -0
  38. data/lib/dm-core/property/numeric.rb +43 -0
  39. data/lib/dm-core/property/object.rb +32 -0
  40. data/lib/dm-core/property/serial.rb +8 -0
  41. data/lib/dm-core/property/string.rb +49 -0
  42. data/lib/dm-core/property/text.rb +12 -0
  43. data/lib/dm-core/property/time.rb +48 -0
  44. data/lib/dm-core/property/typecast/numeric.rb +32 -0
  45. data/lib/dm-core/property/typecast/time.rb +28 -0
  46. data/lib/dm-core/property_set.rb +10 -4
  47. data/lib/dm-core/query.rb +14 -37
  48. data/lib/dm-core/query/conditions/comparison.rb +8 -6
  49. data/lib/dm-core/query/conditions/operation.rb +33 -2
  50. data/lib/dm-core/query/operator.rb +2 -5
  51. data/lib/dm-core/query/path.rb +4 -6
  52. data/lib/dm-core/repository.rb +21 -6
  53. data/lib/dm-core/resource.rb +316 -133
  54. data/lib/dm-core/resource/state.rb +79 -0
  55. data/lib/dm-core/resource/state/clean.rb +40 -0
  56. data/lib/dm-core/resource/state/deleted.rb +30 -0
  57. data/lib/dm-core/resource/state/dirty.rb +86 -0
  58. data/lib/dm-core/resource/state/immutable.rb +34 -0
  59. data/lib/dm-core/resource/state/persisted.rb +29 -0
  60. data/lib/dm-core/resource/state/transient.rb +70 -0
  61. data/lib/dm-core/spec/lib/adapter_helpers.rb +52 -0
  62. data/lib/dm-core/spec/lib/collection_helpers.rb +20 -0
  63. data/{spec → lib/dm-core/spec}/lib/counter_adapter.rb +5 -1
  64. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  65. data/lib/dm-core/spec/lib/spec_helper.rb +68 -0
  66. data/lib/dm-core/spec/setup.rb +165 -0
  67. data/lib/dm-core/spec/{adapter_shared_spec.rb → shared/adapter_spec.rb} +21 -7
  68. data/{spec/public/shared/resource_shared_spec.rb → lib/dm-core/spec/shared/resource_spec.rb} +120 -83
  69. data/{spec/public/shared/sel_shared_spec.rb → lib/dm-core/spec/shared/sel_spec.rb} +5 -6
  70. data/lib/dm-core/support/assertions.rb +8 -0
  71. data/lib/dm-core/support/equalizer.rb +1 -0
  72. data/lib/dm-core/support/hook.rb +420 -0
  73. data/lib/dm-core/support/lazy_array.rb +453 -0
  74. data/lib/dm-core/support/local_object_space.rb +12 -0
  75. data/lib/dm-core/support/logger.rb +193 -6
  76. data/lib/dm-core/support/naming_conventions.rb +8 -8
  77. data/lib/dm-core/support/subject.rb +33 -0
  78. data/lib/dm-core/type.rb +4 -0
  79. data/lib/dm-core/types/boolean.rb +2 -0
  80. data/lib/dm-core/types/decimal.rb +9 -0
  81. data/lib/dm-core/types/discriminator.rb +2 -0
  82. data/lib/dm-core/types/object.rb +3 -0
  83. data/lib/dm-core/types/serial.rb +2 -0
  84. data/lib/dm-core/types/text.rb +2 -0
  85. data/lib/dm-core/version.rb +1 -1
  86. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +67 -0
  87. data/spec/public/model/hook_spec.rb +209 -0
  88. data/spec/public/model/property_spec.rb +35 -0
  89. data/spec/public/model/relationship_spec.rb +33 -20
  90. data/spec/public/model_spec.rb +142 -10
  91. data/spec/public/property/binary_spec.rb +14 -0
  92. data/spec/public/property/boolean_spec.rb +14 -0
  93. data/spec/public/property/class_spec.rb +20 -0
  94. data/spec/public/property/date_spec.rb +14 -0
  95. data/spec/public/property/date_time_spec.rb +14 -0
  96. data/spec/public/property/decimal_spec.rb +14 -0
  97. data/spec/public/{types → property}/discriminator_spec.rb +2 -12
  98. data/spec/public/property/float_spec.rb +14 -0
  99. data/spec/public/property/integer_spec.rb +14 -0
  100. data/spec/public/property/object_spec.rb +9 -17
  101. data/spec/public/property/serial_spec.rb +14 -0
  102. data/spec/public/property/string_spec.rb +14 -0
  103. data/spec/public/property/text_spec.rb +52 -0
  104. data/spec/public/property/time_spec.rb +14 -0
  105. data/spec/public/property_spec.rb +28 -87
  106. data/spec/public/resource_spec.rb +101 -0
  107. data/spec/public/sel_spec.rb +5 -15
  108. data/spec/public/shared/collection_shared_spec.rb +16 -30
  109. data/spec/public/shared/finder_shared_spec.rb +2 -4
  110. data/spec/public/shared/property_shared_spec.rb +176 -0
  111. data/spec/semipublic/adapters/abstract_adapter_spec.rb +1 -1
  112. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +2 -2
  113. data/spec/semipublic/associations/many_to_many_spec.rb +89 -0
  114. data/spec/semipublic/associations/many_to_one_spec.rb +24 -1
  115. data/spec/semipublic/associations/one_to_many_spec.rb +51 -0
  116. data/spec/semipublic/associations/one_to_one_spec.rb +49 -0
  117. data/spec/semipublic/associations/relationship_spec.rb +3 -3
  118. data/spec/semipublic/associations_spec.rb +1 -1
  119. data/spec/semipublic/property/binary_spec.rb +13 -0
  120. data/spec/semipublic/property/boolean_spec.rb +65 -0
  121. data/spec/semipublic/property/class_spec.rb +33 -0
  122. data/spec/semipublic/property/date_spec.rb +43 -0
  123. data/spec/semipublic/property/date_time_spec.rb +46 -0
  124. data/spec/semipublic/property/decimal_spec.rb +82 -0
  125. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  126. data/spec/semipublic/property/float_spec.rb +82 -0
  127. data/spec/semipublic/property/integer_spec.rb +82 -0
  128. data/spec/semipublic/property/serial_spec.rb +13 -0
  129. data/spec/semipublic/property/string_spec.rb +13 -0
  130. data/spec/semipublic/property/text_spec.rb +31 -0
  131. data/spec/semipublic/property/time_spec.rb +50 -0
  132. data/spec/semipublic/property_spec.rb +2 -532
  133. data/spec/semipublic/query/conditions/comparison_spec.rb +171 -169
  134. data/spec/semipublic/query/conditions/operation_spec.rb +53 -51
  135. data/spec/semipublic/query/path_spec.rb +17 -17
  136. data/spec/semipublic/query_spec.rb +47 -78
  137. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  138. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  139. data/spec/semipublic/resource/state/dirty_spec.rb +133 -0
  140. data/spec/semipublic/resource/state/immutable_spec.rb +99 -0
  141. data/spec/semipublic/resource/state/transient_spec.rb +128 -0
  142. data/spec/semipublic/resource/state_spec.rb +226 -0
  143. data/spec/semipublic/shared/property_shared_spec.rb +143 -0
  144. data/spec/semipublic/shared/resource_shared_spec.rb +16 -15
  145. data/spec/semipublic/shared/resource_state_shared_spec.rb +78 -0
  146. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  147. data/spec/spec_helper.rb +21 -97
  148. data/spec/support/types/huge_integer.rb +17 -0
  149. data/spec/unit/array_spec.rb +48 -0
  150. data/spec/unit/hash_spec.rb +35 -0
  151. data/spec/unit/hook_spec.rb +1234 -0
  152. data/spec/unit/lazy_array_spec.rb +1959 -0
  153. data/spec/unit/module_spec.rb +70 -0
  154. data/spec/unit/object_spec.rb +37 -0
  155. data/spec/unit/try_dup_spec.rb +45 -0
  156. data/tasks/local_gemfile.rake +18 -0
  157. data/tasks/spec.rake +0 -3
  158. metadata +197 -71
  159. data/deps.rip +0 -2
  160. data/lib/dm-core/adapters/data_objects_adapter.rb +0 -712
  161. data/lib/dm-core/adapters/mysql_adapter.rb +0 -42
  162. data/lib/dm-core/adapters/oracle_adapter.rb +0 -229
  163. data/lib/dm-core/adapters/postgres_adapter.rb +0 -22
  164. data/lib/dm-core/adapters/sqlite3_adapter.rb +0 -17
  165. data/lib/dm-core/adapters/sqlserver_adapter.rb +0 -114
  166. data/lib/dm-core/adapters/yaml_adapter.rb +0 -111
  167. data/lib/dm-core/core_ext/enumerable.rb +0 -28
  168. data/lib/dm-core/migrations.rb +0 -1427
  169. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +0 -366
  170. data/lib/dm-core/transaction.rb +0 -508
  171. data/lib/dm-core/types/paranoid_boolean.rb +0 -42
  172. data/lib/dm-core/types/paranoid_datetime.rb +0 -41
  173. data/spec/lib/adapter_helpers.rb +0 -105
  174. data/spec/lib/collection_helpers.rb +0 -18
  175. data/spec/lib/pending_helpers.rb +0 -46
  176. data/spec/public/migrations_spec.rb +0 -503
  177. data/spec/public/transaction_spec.rb +0 -153
  178. data/spec/semipublic/adapters/mysql_adapter_spec.rb +0 -17
  179. data/spec/semipublic/adapters/oracle_adapter_spec.rb +0 -194
  180. data/spec/semipublic/adapters/postgres_adapter_spec.rb +0 -17
  181. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +0 -17
  182. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +0 -17
  183. data/spec/semipublic/adapters/yaml_adapter_spec.rb +0 -12
@@ -10,11 +10,11 @@ module DataMapper
10
10
  # TODO: replace this with BasicObject
11
11
  instance_methods.each do |method|
12
12
  next if method =~ /\A__/ ||
13
- %w[ send class dup object_id kind_of? instance_of? respond_to? equal? should should_not instance_variable_set instance_variable_get instance_variable_defined? extend hash inspect copy_object ].include?(method.to_s)
13
+ %w[ send class dup object_id kind_of? instance_of? respond_to? respond_to_missing? equal? freeze frozen? should should_not instance_variables instance_variable_set instance_variable_get instance_variable_defined? remove_instance_variable extend hash inspect copy_object initialize_dup ].include?(method.to_s)
14
14
  undef_method method
15
15
  end
16
16
 
17
- include Extlib::Assertions
17
+ include DataMapper::Assertions
18
18
  extend Equalizer
19
19
 
20
20
  equalize :relationships, :property
@@ -62,16 +62,14 @@ module DataMapper
62
62
 
63
63
  # @api semipublic
64
64
  def initialize(relationships, property_name = nil)
65
- assert_kind_of 'relationships', relationships, Array
66
- assert_kind_of 'property_name', property_name, Symbol, NilClass
67
-
68
- @relationships = relationships.dup
65
+ @relationships = relationships.to_ary.dup
69
66
 
70
67
  last_relationship = @relationships.last
71
68
  @repository_name = last_relationship.relative_target_repository_name
72
69
  @model = last_relationship.target_model
73
70
 
74
71
  if property_name
72
+ property_name = property_name.to_sym
75
73
  @property = @model.properties(@repository_name)[property_name] ||
76
74
  raise(ArgumentError, "Unknown property '#{property_name}' in #{@model}")
77
75
  end
@@ -1,9 +1,9 @@
1
1
  module DataMapper
2
2
  class Repository
3
- include Extlib::Assertions
3
+ include DataMapper::Assertions
4
4
  extend Equalizer
5
5
 
6
- equalize :name, :adapter
6
+ equalize :name
7
7
 
8
8
  # Get the list of adapters registered for all Repositories,
9
9
  # keyed by repository name.
@@ -45,6 +45,9 @@ module DataMapper
45
45
  # @api semipublic
46
46
  attr_reader :name
47
47
 
48
+ # @api semipublic
49
+ alias to_sym name
50
+
48
51
  # Get the adapter for this repository
49
52
  #
50
53
  # Lazy loads adapter setup from registered adapters
@@ -114,6 +117,20 @@ module DataMapper
114
117
  end
115
118
  end
116
119
 
120
+ # Create a Query or subclass instance for this repository.
121
+ #
122
+ # @param [Model] model
123
+ # the Model to retrieve results from
124
+ # @param [Hash] options
125
+ # the conditions and scope
126
+ #
127
+ # @return [Query]
128
+ #
129
+ # @api semipublic
130
+ def new_query(model, options = {})
131
+ adapter.new_query(self, model, options)
132
+ end
133
+
117
134
  # Create one or more resource instances in this repository.
118
135
  #
119
136
  # TODO: create example
@@ -159,7 +176,7 @@ module DataMapper
159
176
  #
160
177
  # @api semipublic
161
178
  def update(attributes, collection)
162
- return 0 unless collection.query.valid?
179
+ return 0 unless collection.query.valid? && attributes.any?
163
180
  adapter.update(attributes, collection)
164
181
  end
165
182
 
@@ -202,9 +219,7 @@ module DataMapper
202
219
  #
203
220
  # @api semipublic
204
221
  def initialize(name)
205
- assert_kind_of 'name', name, Symbol
206
-
207
- @name = name
222
+ @name = name.to_sym
208
223
  @identity_maps = {}
209
224
  end
210
225
  end # class Repository
@@ -1,6 +1,9 @@
1
+ # TODO: DRY up raise_on_save_failure with attr_accessor_with_default
2
+ # once AS branch is merged in
3
+
1
4
  module DataMapper
2
5
  module Resource
3
- include Extlib::Assertions
6
+ include DataMapper::Assertions
4
7
  extend Chainable
5
8
  extend Deprecate
6
9
 
@@ -24,20 +27,51 @@ module DataMapper
24
27
  Model.descendants
25
28
  end
26
29
 
30
+ # Return if Resource#save should raise an exception on save failures (per-resource)
31
+ #
32
+ # This delegates to model.raise_on_save_failure by default.
33
+ #
34
+ # user.raise_on_save_failure # => false
35
+ #
36
+ # @return [Boolean]
37
+ # true if a failure in Resource#save should raise an exception
38
+ #
39
+ # @api public
40
+ def raise_on_save_failure
41
+ if defined?(@raise_on_save_failure)
42
+ @raise_on_save_failure
43
+ else
44
+ model.raise_on_save_failure
45
+ end
46
+ end
47
+
48
+ # Specify if Resource#save should raise an exception on save failures (per-resource)
49
+ #
50
+ # @param [Boolean]
51
+ # a boolean that if true will cause Resource#save to raise an exception
52
+ #
53
+ # @return [Boolean]
54
+ # true if a failure in Resource#save should raise an exception
55
+ #
56
+ # @api public
57
+ def raise_on_save_failure=(raise_on_save_failure)
58
+ @raise_on_save_failure = raise_on_save_failure
59
+ end
60
+
27
61
  # Deprecated API for updating attributes and saving Resource
28
62
  #
29
63
  # @see #update
30
64
  #
31
65
  # @deprecated
32
66
  def update_attributes(attributes = {}, *allowed)
33
- model = self.model
34
- caller = caller[0]
67
+ model = self.model
68
+ call_stack = caller[0]
35
69
 
36
- warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller})"
70
+ warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{call_stack})"
37
71
 
38
72
  if allowed.any?
39
73
  warn "specifying allowed in #{model}#update_attributes is deprecated, " \
40
- "use Hash#only to filter the attributes in the caller (#{caller})"
74
+ "use Hash#only to filter the attributes in the caller (#{call_stack})"
41
75
  attributes = attributes.only(*allowed)
42
76
  end
43
77
 
@@ -55,6 +89,38 @@ module DataMapper
55
89
  # @api public
56
90
  alias_method :model, :class
57
91
 
92
+ # Get the persisted state for the resource
93
+ #
94
+ # @return [Resource::State]
95
+ # the current persisted state for the resource
96
+ #
97
+ # @api private
98
+ def persisted_state
99
+ @_state ||= Resource::State::Transient.new(self)
100
+ end
101
+
102
+ # Set the persisted state for the resource
103
+ #
104
+ # @param [Resource::State]
105
+ # the new persisted state for the resource
106
+ #
107
+ # @return [undefined]
108
+ #
109
+ # @api private
110
+ def persisted_state=(state)
111
+ @_state = state
112
+ end
113
+
114
+ # Test if the persisted state is set
115
+ #
116
+ # @return [Boolean]
117
+ # true if the persisted state is set
118
+ #
119
+ # @api private
120
+ def persisted_state?
121
+ defined?(@_state) ? true : false
122
+ end
123
+
58
124
  # Repository this resource belongs to in the context of this collection
59
125
  # or of the resource's class.
60
126
  #
@@ -98,7 +164,7 @@ module DataMapper
98
164
  #
99
165
  # @api public
100
166
  def new?
101
- !saved?
167
+ persisted_state.kind_of?(State::Transient)
102
168
  end
103
169
 
104
170
  # Checks if this Resource instance is saved
@@ -108,7 +174,7 @@ module DataMapper
108
174
  #
109
175
  # @api public
110
176
  def saved?
111
- @_saved == true
177
+ persisted_state.kind_of?(State::Persisted)
112
178
  end
113
179
 
114
180
  # Checks if this Resource instance is destroyed
@@ -118,7 +184,7 @@ module DataMapper
118
184
  #
119
185
  # @api public
120
186
  def destroyed?
121
- @_destroyed == true
187
+ readonly? && !key.nil?
122
188
  end
123
189
 
124
190
  # Checks if the resource has no changes to save
@@ -128,7 +194,7 @@ module DataMapper
128
194
  #
129
195
  # @api public
130
196
  def clean?
131
- !dirty?
197
+ persisted_state.kind_of?(State::Clean) || persisted_state.kind_of?(State::Immutable)
132
198
  end
133
199
 
134
200
  # Checks if the resource has unsaved changes
@@ -150,7 +216,7 @@ module DataMapper
150
216
  #
151
217
  # @api public
152
218
  def readonly?
153
- @_readonly == true
219
+ persisted_state.kind_of?(State::Immutable)
154
220
  end
155
221
 
156
222
  # Returns the value of the attribute.
@@ -185,7 +251,7 @@ module DataMapper
185
251
  #
186
252
  # @api public
187
253
  def attribute_get(name)
188
- properties[name].get(self)
254
+ persisted_state.get(properties[name])
189
255
  end
190
256
 
191
257
  alias [] attribute_get
@@ -222,13 +288,11 @@ module DataMapper
222
288
  # @param [Object] value
223
289
  # value to store
224
290
  #
225
- # @return [Object]
226
- # the value stored at that given attribute, nil if none,
227
- # and default if necessary
291
+ # @return [undefined]
228
292
  #
229
293
  # @api public
230
294
  def attribute_set(name, value)
231
- properties[name].set(self, value)
295
+ self.persisted_state = persisted_state.set(properties[name], value)
232
296
  end
233
297
 
234
298
  alias []= attribute_set
@@ -283,7 +347,7 @@ module DataMapper
283
347
  raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
284
348
  end
285
349
  when Associations::Relationship, Property
286
- name.set(self, value)
350
+ self.persisted_state = persisted_state.set(name, value)
287
351
  end
288
352
  end
289
353
  end
@@ -307,6 +371,8 @@ module DataMapper
307
371
  clear_subjects
308
372
  end
309
373
 
374
+ self.persisted_state = persisted_state.rollback
375
+
310
376
  self
311
377
  end
312
378
 
@@ -319,7 +385,7 @@ module DataMapper
319
385
  # true if resource and storage state match
320
386
  #
321
387
  # @api public
322
- def update(attributes = {})
388
+ def update(attributes)
323
389
  assert_update_clean_only(:update)
324
390
  self.attributes = attributes
325
391
  save
@@ -334,7 +400,7 @@ module DataMapper
334
400
  # true if resource and storage state match
335
401
  #
336
402
  # @api public
337
- def update!(attributes = {})
403
+ def update!(attributes)
338
404
  assert_update_clean_only(:update!)
339
405
  self.attributes = attributes
340
406
  save!
@@ -348,7 +414,9 @@ module DataMapper
348
414
  # @api public
349
415
  def save
350
416
  assert_not_destroyed(:save)
351
- _save(true)
417
+ retval = _save
418
+ assert_save_successful(:save, retval)
419
+ retval
352
420
  end
353
421
 
354
422
  # Save the instance and loaded, dirty associations to the data-store, bypassing hooks
@@ -359,7 +427,9 @@ module DataMapper
359
427
  # @api public
360
428
  def save!
361
429
  assert_not_destroyed(:save!)
362
- _save(false)
430
+ retval = _save(false)
431
+ assert_save_successful(:save!, retval)
432
+ retval
363
433
  end
364
434
 
365
435
  # Destroy the instance, remove it from the repository
@@ -369,7 +439,13 @@ module DataMapper
369
439
  #
370
440
  # @api public
371
441
  def destroy
372
- destroy!
442
+ return true if destroyed?
443
+ catch :halt do
444
+ before_destroy_hook
445
+ retval = _destroy
446
+ after_destroy_hook
447
+ retval
448
+ end
373
449
  end
374
450
 
375
451
  # Destroy the instance, remove it from the repository, bypassing hooks
@@ -380,15 +456,7 @@ module DataMapper
380
456
  # @api public
381
457
  def destroy!
382
458
  return true if destroyed?
383
-
384
- if saved?
385
- repository.delete(collection_for_self)
386
- reset
387
- @_readonly = true
388
- @_destroyed = true
389
- else
390
- false
391
- end
459
+ _destroy(false)
392
460
  end
393
461
 
394
462
  # Compares another Resource for equality
@@ -423,9 +491,7 @@ module DataMapper
423
491
  # @api public
424
492
  def ==(other)
425
493
  return true if equal?(other)
426
- other.respond_to?(:repository) &&
427
- other.respond_to?(:key) &&
428
- other.respond_to?(:clean?) &&
494
+ return false unless other.kind_of?(Resource) && model.base_model.equal?(other.model.base_model)
429
495
  cmp?(other, :==)
430
496
  end
431
497
 
@@ -443,14 +509,13 @@ module DataMapper
443
509
  def <=>(other)
444
510
  model = self.model
445
511
  unless other.kind_of?(model.base_model)
446
- raise ArgumentError, "Cannot compare a #{other.model} instance with a #{model} instance"
512
+ raise ArgumentError, "Cannot compare a #{other.class} instance with a #{model} instance"
447
513
  end
448
- cmp = 0
449
514
  model.default_order(repository_name).each do |direction|
450
515
  cmp = direction.get(self) <=> direction.get(other)
451
- break if cmp != 0
516
+ return cmp if cmp.nonzero?
452
517
  end
453
- cmp
518
+ 0
454
519
  end
455
520
 
456
521
  # Returns hash value of the object.
@@ -497,7 +562,11 @@ module DataMapper
497
562
  #
498
563
  # @api semipublic
499
564
  def original_attributes
500
- @_original_attributes ||= {}
565
+ if persisted_state.respond_to?(:original_attributes)
566
+ persisted_state.original_attributes.dup.freeze
567
+ else
568
+ {}.freeze
569
+ end
501
570
  end
502
571
 
503
572
  # Checks if an attribute has been loaded from the repository
@@ -546,24 +615,13 @@ module DataMapper
546
615
  dirty_attributes = {}
547
616
 
548
617
  original_attributes.each_key do |property|
549
- dirty_attributes[property] = property.value(property.get!(self))
618
+ next unless property.respond_to?(:value)
619
+ dirty_attributes[property] = property.dump(property.get!(self))
550
620
  end
551
621
 
552
622
  dirty_attributes
553
623
  end
554
624
 
555
- # Reset the Resource to a similar state as a new record:
556
- # removes it from identity map and clears original property
557
- # values (thus making all properties non dirty)
558
- #
559
- # @api private
560
- def reset
561
- @_saved = false
562
- remove_from_identity_map
563
- original_attributes.clear
564
- self
565
- end
566
-
567
625
  # Returns the Collection the Resource is associated with
568
626
  #
569
627
  # @return [nil]
@@ -609,29 +667,81 @@ module DataMapper
609
667
  #
610
668
  # @api semipublic
611
669
  def query
612
- Query.new(repository, model, :fields => fields, :conditions => conditions)
670
+ repository.new_query(model, :fields => fields, :conditions => conditions)
613
671
  end
614
672
 
615
673
  protected
616
674
 
617
- # Method for hooking callbacks on resource creation
675
+ # Method for hooking callbacks before resource saving
618
676
  #
619
- # @return [Boolean]
620
- # true if the create was successful, false if not
677
+ # @return [undefined]
621
678
  #
622
679
  # @api private
623
- def create_hook
624
- _create
680
+ def before_save_hook
681
+ execute_hooks_for(:before, :save)
625
682
  end
626
683
 
627
- # Method for hooking callbacks on resource updates
684
+ # Method for hooking callbacks after resource saving
628
685
  #
629
- # @return [Boolean]
630
- # true if the update was successful, false if not
686
+ # @return [undefined]
687
+ #
688
+ # @api private
689
+ def after_save_hook
690
+ execute_hooks_for(:after, :save)
691
+ end
692
+
693
+ # Method for hooking callbacks before resource creation
694
+ #
695
+ # @return [undefined]
696
+ #
697
+ # @api private
698
+ def before_create_hook
699
+ execute_hooks_for(:before, :create)
700
+ end
701
+
702
+ # Method for hooking callbacks after resource creation
703
+ #
704
+ # @return [undefined]
705
+ #
706
+ # @api private
707
+ def after_create_hook
708
+ execute_hooks_for(:after, :create)
709
+ end
710
+
711
+ # Method for hooking callbacks before resource updating
712
+ #
713
+ # @return [undefined]
631
714
  #
632
715
  # @api private
633
- def update_hook
634
- _update
716
+ def before_update_hook
717
+ execute_hooks_for(:before, :update)
718
+ end
719
+
720
+ # Method for hooking callbacks after resource updating
721
+ #
722
+ # @return [undefined]
723
+ #
724
+ # @api private
725
+ def after_update_hook
726
+ execute_hooks_for(:after, :update)
727
+ end
728
+
729
+ # Method for hooking callbacks before resource destruction
730
+ #
731
+ # @return [undefined]
732
+ #
733
+ # @api private
734
+ def before_destroy_hook
735
+ execute_hooks_for(:before, :destroy)
736
+ end
737
+
738
+ # Method for hooking callbacks after resource destruction
739
+ #
740
+ # @return [undefined]
741
+ #
742
+ # @api private
743
+ def after_destroy_hook
744
+ execute_hooks_for(:after, :destroy)
635
745
  end
636
746
 
637
747
  private
@@ -649,6 +759,15 @@ module DataMapper
649
759
  self.attributes = attributes
650
760
  end
651
761
 
762
+ # @api private
763
+ def initialize_copy(original)
764
+ instance_variables.each do |ivar|
765
+ instance_variable_set(ivar, instance_variable_get(ivar).try_dup)
766
+ end
767
+
768
+ self.persisted_state = persisted_state.class.new(self)
769
+ end
770
+
652
771
  # Returns name of the repository this object
653
772
  # was loaded from
654
773
  #
@@ -721,7 +840,6 @@ module DataMapper
721
840
  def reset_key
722
841
  properties.key.zip(key) do |property, value|
723
842
  property.set!(self, value)
724
- original_attributes.delete(property)
725
843
  end
726
844
  end
727
845
 
@@ -736,7 +854,6 @@ module DataMapper
736
854
  (model_properties - model_properties.key | relationships.values).each do |subject|
737
855
  next unless subject.loaded?(self)
738
856
  remove_instance_variable(subject.instance_variable_name)
739
- original_attributes.delete(subject)
740
857
  end
741
858
  end
742
859
 
@@ -762,7 +879,10 @@ module DataMapper
762
879
  #
763
880
  # @api private
764
881
  def eager_load(properties)
765
- unless properties.empty? || key.nil?
882
+ unless properties.empty? || key.nil? || collection.nil?
883
+ # set an initial value to prevent recursive lazy loads
884
+ properties.each { |property| property.set!(self, nil) }
885
+
766
886
  collection.reload(:fields => properties)
767
887
  end
768
888
 
@@ -794,7 +914,10 @@ module DataMapper
794
914
  parent_relationships = []
795
915
 
796
916
  relationships.each_value do |relationship|
797
- next unless relationship.respond_to?(:resource_for) && relationship.loaded?(self) && relationship.get!(self)
917
+ next unless relationship.respond_to?(:resource_for)
918
+ set_default_value(relationship)
919
+ next unless relationship.loaded?(self) && relationship.get!(self)
920
+
798
921
  parent_relationships << relationship
799
922
  end
800
923
 
@@ -811,7 +934,10 @@ module DataMapper
811
934
  child_relationships = []
812
935
 
813
936
  relationships.each_value do |relationship|
814
- next unless relationship.respond_to?(:collection_for) && relationship.loaded?(self) && relationship.get!(self)
937
+ next unless relationship.respond_to?(:collection_for)
938
+ set_default_value(relationship)
939
+ next unless relationship.loaded?(self)
940
+
815
941
  child_relationships << relationship
816
942
  end
817
943
 
@@ -823,13 +949,13 @@ module DataMapper
823
949
  end
824
950
 
825
951
  # @api private
826
- def parent_resources
952
+ def parent_associations
827
953
  parent_relationships.map { |relationship| relationship.get!(self) }
828
954
  end
829
955
 
830
956
  # @api private
831
- def child_collections
832
- child_relationships.map { |relationship| relationship.get!(self) }
957
+ def child_associations
958
+ child_relationships.map { |relationship| relationship.get_collection(self) }
833
959
  end
834
960
 
835
961
  # Creates the resource with default values
@@ -850,26 +976,26 @@ module DataMapper
850
976
  #
851
977
  # @api private
852
978
  def _create
853
- # Can't create a resource that is not dirty and doesn't have serial keys
854
- return false if new? && clean?
979
+ self.persisted_state = persisted_state.commit
980
+ true
981
+ end
855
982
 
856
- # set defaults for new resource
857
- properties.each do |property|
858
- unless property.serial? || property.loaded?(self)
859
- property.set(self, property.default_for(self))
860
- end
983
+ # This method executes the hooks before and after resource creation
984
+ #
985
+ # @return [Boolean]
986
+ #
987
+ # @see Resource#_create
988
+ #
989
+ # @api private
990
+ def create_with_hooks
991
+ catch :halt do
992
+ before_save_hook
993
+ before_create_hook
994
+ retval = _create
995
+ after_create_hook
996
+ after_save_hook
997
+ retval
861
998
  end
862
-
863
- @_repository = repository
864
- @_repository.create([ self ])
865
-
866
- @_saved = true
867
-
868
- original_attributes.clear
869
-
870
- add_to_identity_map
871
-
872
- true
873
999
  end
874
1000
 
875
1001
  # Updates resource state
@@ -883,33 +1009,39 @@ module DataMapper
883
1009
  #
884
1010
  # @api private
885
1011
  def _update
886
- original_attributes = self.original_attributes
887
-
888
- if original_attributes.empty?
889
- true
890
- elsif original_attributes.any? { |property, _value| !property.valid?(property.get!(self)) }
891
- false
892
- else
893
- # remove from the identity map
894
- remove_from_identity_map
895
-
896
- repository.update(dirty_attributes, collection_for_self)
897
-
898
- original_attributes.clear
899
-
900
- # remove the cached key in case it is updated
901
- remove_instance_variable(:@_key)
902
-
903
- add_to_identity_map
1012
+ self.persisted_state = persisted_state.commit if valid_attributes?
1013
+ clean?
1014
+ end
904
1015
 
905
- true
1016
+ # This method executes the hooks before and after resource updating
1017
+ #
1018
+ # @return [Boolean]
1019
+ #
1020
+ # @see Resource#_update
1021
+ #
1022
+ # @api private
1023
+ def update_with_hooks
1024
+ catch :halt do
1025
+ before_save_hook
1026
+ before_update_hook
1027
+ retval = _update
1028
+ after_update_hook
1029
+ after_save_hook
1030
+ retval
906
1031
  end
907
1032
  end
908
1033
 
909
1034
  # @api private
910
- def _save(safe)
1035
+ def _destroy(execute_hooks = true)
1036
+ deleted = persisted_state.delete
1037
+ self.persisted_state = deleted.commit
1038
+ true
1039
+ end
1040
+
1041
+ # @api private
1042
+ def _save(execute_hooks = true)
911
1043
  run_once(true) do
912
- save_parents(safe) && save_self(safe) && save_children(safe)
1044
+ save_parents(execute_hooks) && save_self(execute_hooks) && save_children(execute_hooks)
913
1045
  end
914
1046
  end
915
1047
 
@@ -919,10 +1051,13 @@ module DataMapper
919
1051
  # true if the resource was successfully saved
920
1052
  #
921
1053
  # @api semipublic
922
- def save_self(safe = true)
1054
+ def save_self(execute_hooks = true)
1055
+ # short-circuit if the resource is not dirty
1056
+ return saved? unless dirty_self?
1057
+
923
1058
  new_resource = new?
924
- if safe
925
- new_resource ? create_hook : update_hook
1059
+ if execute_hooks
1060
+ new_resource ? create_with_hooks : update_with_hooks
926
1061
  else
927
1062
  new_resource ? _create : _update
928
1063
  end
@@ -934,15 +1069,15 @@ module DataMapper
934
1069
  # true if the parents were successfully saved
935
1070
  #
936
1071
  # @api private
937
- def save_parents(safe)
1072
+ def save_parents(execute_hooks)
938
1073
  run_once(true) do
939
- parent_relationships.all? do |relationship|
1074
+ parent_relationships.map do |relationship|
940
1075
  parent = relationship.get!(self)
941
1076
 
942
- if parent.__send__(:save_parents, safe) && parent.__send__(:save_self, safe)
1077
+ if parent.__send__(:save_parents, execute_hooks) && parent.__send__(:save_self, execute_hooks)
943
1078
  relationship.set(self, parent) # set the FK values
944
1079
  end
945
- end
1080
+ end.all?
946
1081
  end
947
1082
  end
948
1083
 
@@ -952,10 +1087,10 @@ module DataMapper
952
1087
  # true if the children were successfully saved
953
1088
  #
954
1089
  # @api private
955
- def save_children(safe)
956
- child_collections.all? do |collection|
957
- collection.send(safe ? :save : :save!)
958
- end
1090
+ def save_children(execute_hooks)
1091
+ child_associations.map do |association|
1092
+ association.__send__(execute_hooks ? :save : :save!)
1093
+ end.all?
959
1094
  end
960
1095
 
961
1096
  # Checks if the resource has unsaved changes
@@ -963,7 +1098,7 @@ module DataMapper
963
1098
  # @return [Boolean]
964
1099
  # true if the resource has unsaged changes
965
1100
  #
966
- # @api private
1101
+ # @api semipublic
967
1102
  def dirty_self?
968
1103
  if original_attributes.any?
969
1104
  true
@@ -982,8 +1117,8 @@ module DataMapper
982
1117
  # @api private
983
1118
  def dirty_parents?
984
1119
  run_once(false) do
985
- parent_resources.any? do |parent|
986
- parent.__send__(:dirty_self?) || parent.__send__(:dirty_parents?)
1120
+ parent_associations.any? do |association|
1121
+ association.__send__(:dirty_self?) || association.__send__(:dirty_parents?)
987
1122
  end
988
1123
  end
989
1124
  end
@@ -998,7 +1133,7 @@ module DataMapper
998
1133
  #
999
1134
  # @api private
1000
1135
  def dirty_children?
1001
- child_collections.any? { |children| children.dirty? }
1136
+ child_associations.any? { |association| association.dirty? }
1002
1137
  end
1003
1138
 
1004
1139
  # Return true if +other+'s is equivalent or equal to +self+'s
@@ -1013,17 +1148,46 @@ module DataMapper
1013
1148
  #
1014
1149
  # @api private
1015
1150
  def cmp?(other, operator)
1016
- return false unless key.send(operator, other.key)
1017
- return true if repository.send(operator, other.repository) && clean? && other.clean?
1151
+ return false unless repository.send(operator, other.repository) &&
1152
+ key.send(operator, other.key)
1018
1153
 
1019
- # get all the loaded and non-loaded properties that are not keys,
1020
- # since the key comparison was performed earlier
1021
- loaded, not_loaded = properties.select { |property| !property.key? }.partition do |property|
1022
- property.loaded?(self) && property.loaded?(other)
1154
+ if saved? && other.saved?
1155
+ # if dirty attributes match then they are the same resource
1156
+ dirty_attributes == other.dirty_attributes
1157
+ else
1158
+ # compare properties for unsaved resources
1159
+ properties.all? do |property|
1160
+ __send__(property.name).send(operator, other.__send__(property.name))
1161
+ end
1023
1162
  end
1163
+ end
1024
1164
 
1025
- # check all loaded properties, and then all unloaded properties
1026
- (loaded + not_loaded).all? { |property| property.get(self).send(operator, property.get(other)) }
1165
+ # @api private
1166
+ def set_default_value(subject)
1167
+ return unless persisted_state.respond_to?(:set_default_value, true)
1168
+ persisted_state.__send__(:set_default_value, subject)
1169
+ end
1170
+
1171
+ # @api private
1172
+ def valid_attributes?
1173
+ original_attributes.each_key do |property|
1174
+ return false if property.kind_of?(Property) && !property.valid?(property.get!(self))
1175
+ end
1176
+ true
1177
+ end
1178
+
1179
+ # Execute all the queued up hooks for a given type and name
1180
+ #
1181
+ # @param [Symbol] type
1182
+ # the type of hook to execute (before or after)
1183
+ # @param [Symbol] name
1184
+ # the name of the hook to execute
1185
+ #
1186
+ # @return [undefined]
1187
+ #
1188
+ # @api private
1189
+ def execute_hooks_for(type, name)
1190
+ model.hooks[name][type].each { |hook| hook.call(self) }
1027
1191
  end
1028
1192
 
1029
1193
  # Raises an exception if #update is performed on a dirty resource
@@ -1039,7 +1203,7 @@ module DataMapper
1039
1203
  # @api private
1040
1204
  def assert_update_clean_only(method)
1041
1205
  if dirty?
1042
- raise UpdateConflictError, "#{model}##{method} cannot be called on a dirty resource"
1206
+ raise UpdateConflictError, "#{model}##{method} cannot be called on a #{new? ? 'new' : 'dirty'} resource"
1043
1207
  end
1044
1208
  end
1045
1209
 
@@ -1060,6 +1224,25 @@ module DataMapper
1060
1224
  end
1061
1225
  end
1062
1226
 
1227
+ # Raises an exception if #save returns false
1228
+ #
1229
+ # @param [Symbol] method
1230
+ # the name of the method to use in the exception
1231
+ # @param [Boolean] save_result
1232
+ # the result of the #save call
1233
+ #
1234
+ # @return [undefined]
1235
+ #
1236
+ # @raise [SaveFailureError]
1237
+ # raise if the resource was not saved
1238
+ #
1239
+ # @api private
1240
+ def assert_save_successful(method, save_retval)
1241
+ if save_retval != true && raise_on_save_failure
1242
+ raise SaveFailureError, "#{model}##{method} returned #{save_retval.inspect}, #{model} was not saved"
1243
+ end
1244
+ end
1245
+
1063
1246
  # Prevent a method from being in the stack more than once
1064
1247
  #
1065
1248
  # The purpose of this method is to prevent SystemStackError from