dm-core 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (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