dm-core 0.9.11 → 0.10.0

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