datamapper-dm-core 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/.autotest +26 -0
  2. data/.gitignore +18 -0
  3. data/CONTRIBUTING +51 -0
  4. data/FAQ +92 -0
  5. data/History.txt +41 -0
  6. data/MIT-LICENSE +22 -0
  7. data/Manifest.txt +130 -0
  8. data/QUICKLINKS +11 -0
  9. data/README.txt +143 -0
  10. data/Rakefile +30 -0
  11. data/SPECS +62 -0
  12. data/TODO +1 -0
  13. data/dm-core.gemspec +40 -0
  14. data/lib/dm-core.rb +217 -0
  15. data/lib/dm-core/adapters.rb +16 -0
  16. data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
  17. data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
  18. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  19. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  22. data/lib/dm-core/associations.rb +207 -0
  23. data/lib/dm-core/associations/many_to_many.rb +147 -0
  24. data/lib/dm-core/associations/many_to_one.rb +107 -0
  25. data/lib/dm-core/associations/one_to_many.rb +315 -0
  26. data/lib/dm-core/associations/one_to_one.rb +61 -0
  27. data/lib/dm-core/associations/relationship.rb +229 -0
  28. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  29. data/lib/dm-core/auto_migrations.rb +105 -0
  30. data/lib/dm-core/collection.rb +670 -0
  31. data/lib/dm-core/dependency_queue.rb +32 -0
  32. data/lib/dm-core/hook.rb +11 -0
  33. data/lib/dm-core/identity_map.rb +42 -0
  34. data/lib/dm-core/is.rb +16 -0
  35. data/lib/dm-core/logger.rb +232 -0
  36. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  37. data/lib/dm-core/migrator.rb +29 -0
  38. data/lib/dm-core/model.rb +526 -0
  39. data/lib/dm-core/naming_conventions.rb +84 -0
  40. data/lib/dm-core/property.rb +676 -0
  41. data/lib/dm-core/property_set.rb +169 -0
  42. data/lib/dm-core/query.rb +676 -0
  43. data/lib/dm-core/repository.rb +167 -0
  44. data/lib/dm-core/resource.rb +671 -0
  45. data/lib/dm-core/scope.rb +58 -0
  46. data/lib/dm-core/support.rb +7 -0
  47. data/lib/dm-core/support/array.rb +13 -0
  48. data/lib/dm-core/support/assertions.rb +8 -0
  49. data/lib/dm-core/support/errors.rb +23 -0
  50. data/lib/dm-core/support/kernel.rb +11 -0
  51. data/lib/dm-core/support/symbol.rb +41 -0
  52. data/lib/dm-core/transaction.rb +267 -0
  53. data/lib/dm-core/type.rb +160 -0
  54. data/lib/dm-core/type_map.rb +80 -0
  55. data/lib/dm-core/types.rb +19 -0
  56. data/lib/dm-core/types/boolean.rb +7 -0
  57. data/lib/dm-core/types/discriminator.rb +34 -0
  58. data/lib/dm-core/types/object.rb +24 -0
  59. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  60. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  61. data/lib/dm-core/types/serial.rb +9 -0
  62. data/lib/dm-core/types/text.rb +10 -0
  63. data/lib/dm-core/version.rb +3 -0
  64. data/script/all +4 -0
  65. data/script/performance.rb +282 -0
  66. data/script/profile.rb +87 -0
  67. data/spec/integration/association_spec.rb +1382 -0
  68. data/spec/integration/association_through_spec.rb +203 -0
  69. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  70. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  71. data/spec/integration/associations/one_to_many_spec.rb +188 -0
  72. data/spec/integration/auto_migrations_spec.rb +413 -0
  73. data/spec/integration/collection_spec.rb +1073 -0
  74. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  75. data/spec/integration/dependency_queue_spec.rb +46 -0
  76. data/spec/integration/model_spec.rb +197 -0
  77. data/spec/integration/mysql_adapter_spec.rb +85 -0
  78. data/spec/integration/postgres_adapter_spec.rb +731 -0
  79. data/spec/integration/property_spec.rb +253 -0
  80. data/spec/integration/query_spec.rb +514 -0
  81. data/spec/integration/repository_spec.rb +61 -0
  82. data/spec/integration/resource_spec.rb +513 -0
  83. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  84. data/spec/integration/sti_spec.rb +273 -0
  85. data/spec/integration/strategic_eager_loading_spec.rb +156 -0
  86. data/spec/integration/transaction_spec.rb +75 -0
  87. data/spec/integration/type_spec.rb +275 -0
  88. data/spec/lib/logging_helper.rb +18 -0
  89. data/spec/lib/mock_adapter.rb +27 -0
  90. data/spec/lib/model_loader.rb +100 -0
  91. data/spec/lib/publicize_methods.rb +28 -0
  92. data/spec/models/content.rb +16 -0
  93. data/spec/models/vehicles.rb +34 -0
  94. data/spec/models/zoo.rb +48 -0
  95. data/spec/spec.opts +3 -0
  96. data/spec/spec_helper.rb +91 -0
  97. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  98. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  99. data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
  100. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  101. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  102. data/spec/unit/associations/many_to_many_spec.rb +32 -0
  103. data/spec/unit/associations/many_to_one_spec.rb +159 -0
  104. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  105. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  106. data/spec/unit/associations/relationship_spec.rb +71 -0
  107. data/spec/unit/associations_spec.rb +242 -0
  108. data/spec/unit/auto_migrations_spec.rb +111 -0
  109. data/spec/unit/collection_spec.rb +182 -0
  110. data/spec/unit/data_mapper_spec.rb +35 -0
  111. data/spec/unit/identity_map_spec.rb +126 -0
  112. data/spec/unit/is_spec.rb +80 -0
  113. data/spec/unit/migrator_spec.rb +33 -0
  114. data/spec/unit/model_spec.rb +321 -0
  115. data/spec/unit/naming_conventions_spec.rb +36 -0
  116. data/spec/unit/property_set_spec.rb +90 -0
  117. data/spec/unit/property_spec.rb +753 -0
  118. data/spec/unit/query_spec.rb +571 -0
  119. data/spec/unit/repository_spec.rb +93 -0
  120. data/spec/unit/resource_spec.rb +649 -0
  121. data/spec/unit/scope_spec.rb +142 -0
  122. data/spec/unit/transaction_spec.rb +493 -0
  123. data/spec/unit/type_map_spec.rb +114 -0
  124. data/spec/unit/type_spec.rb +119 -0
  125. data/tasks/ci.rb +36 -0
  126. data/tasks/dm.rb +63 -0
  127. data/tasks/doc.rb +20 -0
  128. data/tasks/gemspec.rb +23 -0
  129. data/tasks/hoe.rb +46 -0
  130. data/tasks/install.rb +20 -0
  131. metadata +215 -0
@@ -0,0 +1,167 @@
1
+ module DataMapper
2
+ class Repository
3
+ include Assertions
4
+
5
+ @adapters = {}
6
+
7
+ ##
8
+ #
9
+ # @return <Adapter> the adapters registered for this repository
10
+ def self.adapters
11
+ @adapters
12
+ end
13
+
14
+ def self.context
15
+ Thread.current[:dm_repository_contexts] ||= []
16
+ end
17
+
18
+ def self.default_name
19
+ :default
20
+ end
21
+
22
+ attr_reader :name
23
+
24
+ def adapter
25
+ # Make adapter instantiation lazy so we can defer repository setup until it's actually
26
+ # 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)
30
+
31
+ self.class.adapters[@name]
32
+ end
33
+ end
34
+
35
+ def identity_map(model)
36
+ @identity_maps[model] ||= IdentityMap.new
37
+ end
38
+
39
+ # TODO: spec this
40
+ def scope
41
+ Repository.context << self
42
+
43
+ begin
44
+ return yield(self)
45
+ ensure
46
+ Repository.context.pop
47
+ end
48
+ end
49
+
50
+ def create(resources)
51
+ adapter.create(resources)
52
+ end
53
+
54
+ ##
55
+ # retrieve a collection of results of a query
56
+ #
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
66
+ #
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)
73
+ end
74
+
75
+ def update(attributes, query)
76
+ adapter.update(attributes, query)
77
+ end
78
+
79
+ def delete(query)
80
+ adapter.delete(query)
81
+ end
82
+
83
+ def eql?(other)
84
+ return true if super
85
+ name == other.name
86
+ end
87
+
88
+ alias == eql?
89
+
90
+ def to_s
91
+ "#<DataMapper::Repository:#{@name}>"
92
+ end
93
+
94
+ def _dump(*)
95
+ name.to_s
96
+ end
97
+
98
+ def self._load(marshalled)
99
+ new(marshalled.to_sym)
100
+ end
101
+
102
+ private
103
+
104
+ def initialize(name)
105
+ assert_kind_of 'name', name, Symbol
106
+
107
+ @name = name
108
+ @identity_maps = {}
109
+ end
110
+
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)
121
+ end
122
+
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)
130
+ end
131
+
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
146
+ 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
+ end # class Repository
167
+ end # module DataMapper
@@ -0,0 +1,671 @@
1
+ require 'set'
2
+
3
+ module DataMapper
4
+ module Resource
5
+ include Assertions
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
23
+ def self.append_inclusions(*inclusions)
24
+ extra_inclusions.concat inclusions
25
+ true
26
+ end
27
+
28
+ def self.extra_inclusions
29
+ @extra_inclusions ||= []
30
+ end
31
+
32
+ # When Resource is included in a class this method makes sure
33
+ # it gets all the methods
34
+ #
35
+ # -
36
+ # @api private
37
+ def self.included(model)
38
+ 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
+ end
48
+
49
+ # Return all classes that include the DataMapper::Resource module
50
+ #
51
+ # ==== Returns
52
+ # Set:: a set containing the including classes
53
+ #
54
+ # ==== Example
55
+ #
56
+ # Class Foo
57
+ # include DataMapper::Resource
58
+ # end
59
+ #
60
+ # DataMapper::Resource.descendants.to_a.first == Foo
61
+ #
62
+ # -
63
+ # @api semipublic
64
+ def self.descendants
65
+ @descendants ||= Set.new
66
+ end
67
+
68
+ # +---------------
69
+ # Instance methods
70
+
71
+ attr_writer :collection
72
+
73
+ alias model class
74
+
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.
78
+ #
79
+ # ==== Parameters
80
+ # name<Symbol>:: name attribute to lookup
81
+ #
82
+ # ==== Returns
83
+ # <Types>:: the value stored at that given attribute, nil if none, and default if necessary
84
+ #
85
+ # ==== Example
86
+ #
87
+ # Class Foo
88
+ # include DataMapper::Resource
89
+ #
90
+ # property :first_name, String
91
+ # property :last_name, String
92
+ #
93
+ # def full_name
94
+ # "#{attribute_get(:first_name)} #{attribute_get(:last_name)}"
95
+ # end
96
+ #
97
+ # # using the shorter syntax
98
+ # def name_for_address_book
99
+ # "#{last_name}, #{first_name}"
100
+ # end
101
+ # end
102
+ #
103
+ # -
104
+ # @api semipublic
105
+ def attribute_get(name)
106
+ properties[name].get(self)
107
+ end
108
+
109
+ # sets the value of the attribute and marks the attribute as dirty
110
+ # if it has been changed so that it may be saved. Do not set from
111
+ # instance variables directly, but use this method. This method
112
+ # handels the lazy loading the property and returning of defaults
113
+ # if nessesary.
114
+ #
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
125
+ # include DataMapper::Resource
126
+ #
127
+ # property :first_name, String
128
+ # property :last_name, String
129
+ #
130
+ # def full_name(name)
131
+ # name = name.split(' ')
132
+ # attribute_set(:first_name, name[0])
133
+ # attribute_set(:last_name, name[1])
134
+ # end
135
+ #
136
+ # # using the shorter syntax
137
+ # def name_from_address_book(name)
138
+ # name = name.split(', ')
139
+ # first_name = name[1]
140
+ # last_name = name[0]
141
+ # end
142
+ # end
143
+ #
144
+ # -
145
+ # @api semipublic
146
+ def attribute_set(name, value)
147
+ properties[name].set(self, value)
148
+ end
149
+
150
+ # Compares if its the same object or if attributes are equal
151
+ #
152
+ # The comparaison is
153
+ # * false if object not from same repository
154
+ # * false if object has no all same properties
155
+ #
156
+ #
157
+ # ==== Parameters
158
+ # other<Object>:: Object to compare to
159
+ #
160
+ # ==== Returns
161
+ # <True>:: the outcome of the comparison as a boolean
162
+ #
163
+ # -
164
+ # @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)
181
+ 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
+ end
186
+
187
+ alias == eql?
188
+
189
+ # Computes a hash for the resource
190
+ #
191
+ # ==== Returns
192
+ # <Integer>:: the hash value of the resource
193
+ #
194
+ # -
195
+ # @api public
196
+ def hash
197
+ model.hash + key.hash
198
+ end
199
+
200
+ # Inspection of the class name and the attributes
201
+ #
202
+ # ==== Returns
203
+ # <String>:: with the class name, attributes with their values
204
+ #
205
+ # ==== Example
206
+ #
207
+ # >> Foo.new
208
+ # => #<Foo name=nil updated_at=nil created_at=nil id=nil>
209
+ #
210
+ # -
211
+ # @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}"
223
+ end
224
+
225
+ "#<#{model.name} #{attrs * ' '}>"
226
+ end
227
+
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
237
+ end
238
+ end
239
+
240
+ ##
241
+ #
242
+ # ==== Returns
243
+ # <Repository>:: the respository this resource belongs to in the context of a collection OR in the class's context
244
+ #
245
+ # @api public
246
+ def repository
247
+ @repository || model.repository
248
+ end
249
+
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
253
+ #
254
+ # ==== Returns
255
+ # <Array[Key], Key> key or keys
256
+ #
257
+ # --
258
+ # @api public
259
+ def id
260
+ key = self.key
261
+ key.first if key.size == 1
262
+ end
263
+
264
+ def key
265
+ key_properties.map do |property|
266
+ original_values[property.name] || property.get!(self)
267
+ end
268
+ end
269
+
270
+ def readonly!
271
+ @readonly = true
272
+ end
273
+
274
+ def readonly?
275
+ @readonly == true
276
+ end
277
+
278
+ # save the instance to the data-store
279
+ #
280
+ # ==== Returns
281
+ # <True, False>:: results of the save
282
+ #
283
+ # @see DataMapper::Repository#save
284
+ #
285
+ # --
286
+ # #public
287
+ def save(context = :default)
288
+ # Takes a context, but does nothing with it. This is to maintain the
289
+ # same API through out all of dm-more. dm-validations requires a
290
+ # context to be passed
291
+
292
+ associations_saved = false
293
+ child_associations.each { |a| associations_saved |= a.save }
294
+
295
+ saved = new_record? ? create : update
296
+
297
+ if saved
298
+ original_values.clear
299
+ end
300
+
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
306
+ end
307
+
308
+ # destroy the instance, remove it from the repository
309
+ #
310
+ # ==== Returns
311
+ # <True, False>:: results of the destruction
312
+ #
313
+ # --
314
+ # @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
322
+
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)
326
+ end
327
+
328
+ true
329
+ end
330
+
331
+ # Checks if the attribute has been loaded
332
+ #
333
+ # ==== Example
334
+ #
335
+ # class Foo
336
+ # include DataMapper::Resource
337
+ # property :name, String
338
+ # property :description, Text, :lazy => false
339
+ # end
340
+ #
341
+ # Foo.new.attribute_loaded?(:description) # will return false
342
+ #
343
+ # --
344
+ # @api public
345
+ def attribute_loaded?(name)
346
+ instance_variable_defined?(properties[name].instance_variable_name)
347
+ end
348
+
349
+ # fetches all the names of the attributes that have been loaded,
350
+ # even if they are lazy but have been called
351
+ #
352
+ # ==== Returns
353
+ # Array[<Symbol>]:: names of attributes that have been loaded
354
+ #
355
+ # ==== Example
356
+ #
357
+ # class Foo
358
+ # include DataMapper::Resource
359
+ # property :name, String
360
+ # property :description, Text, :lazy => false
361
+ # end
362
+ #
363
+ # Foo.new.loaded_attributes # returns [:name]
364
+ #
365
+ # --
366
+ # @api public
367
+ def loaded_attributes
368
+ properties.map{|p| p.name if attribute_loaded?(p.name)}.compact
369
+ end
370
+
371
+ # set of original values of properties
372
+ #
373
+ # ==== Returns
374
+ # Hash:: original values of properties
375
+ #
376
+ # --
377
+ # @api public
378
+ def original_values
379
+ @original_values ||= {}
380
+ end
381
+
382
+ # Hash of attributes that have been marked dirty
383
+ #
384
+ # ==== Returns
385
+ # Hash:: attributes that have been marked dirty
386
+ #
387
+ # --
388
+ # @api private
389
+ def dirty_attributes
390
+ dirty_attributes = {}
391
+ properties = self.properties
392
+
393
+ original_values.each do |name, old_value|
394
+ property = properties[name]
395
+ new_value = property.get!(self)
396
+
397
+ dirty = case property.track
398
+ when :hash then old_value != new_value.hash
399
+ else
400
+ property.value(old_value) != property.value(new_value)
401
+ end
402
+
403
+ if dirty
404
+ property.hash
405
+ dirty_attributes[property] = property.value(new_value)
406
+ end
407
+ end
408
+
409
+ dirty_attributes
410
+ end
411
+
412
+ # Checks if the class is dirty
413
+ #
414
+ # ==== Returns
415
+ # True:: returns if class is dirty
416
+ #
417
+ # --
418
+ # @api public
419
+ def dirty?
420
+ dirty_attributes.any?
421
+ end
422
+
423
+ # Checks if the attribute is dirty
424
+ #
425
+ # ==== Parameters
426
+ # name<Symbol>:: name of attribute
427
+ #
428
+ # ==== Returns
429
+ # True:: returns if attribute is dirty
430
+ #
431
+ # --
432
+ # @api public
433
+ def attribute_dirty?(name)
434
+ dirty_attributes.has_key?(properties[name])
435
+ end
436
+
437
+ def collection
438
+ @collection ||= if query = to_query
439
+ Collection.new(query) { |c| c << self }
440
+ end
441
+ end
442
+
443
+ # Reload association and all child association
444
+ #
445
+ # ==== Returns
446
+ # self:: returns the class itself
447
+ #
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
455
+
456
+ self
457
+ end
458
+
459
+ # Reload specific attributes
460
+ #
461
+ # ==== Parameters
462
+ # *attributes<Array[<Symbol>]>:: name of attribute
463
+ #
464
+ # ==== Returns
465
+ # self:: returns the class itself
466
+ #
467
+ # --
468
+ # @api public
469
+ def reload_attributes(*attributes)
470
+ unless attributes.empty? || new_record?
471
+ collection.reload(:fields => attributes)
472
+ end
473
+
474
+ self
475
+ end
476
+
477
+ # Checks if the model has been saved
478
+ #
479
+ # ==== Returns
480
+ # True:: status if the model is new
481
+ #
482
+ # --
483
+ # @api public
484
+ def new_record?
485
+ !defined?(@new_record) || @new_record
486
+ end
487
+
488
+ # all the attributes of the model
489
+ #
490
+ # ==== Returns
491
+ # Hash[<Symbol>]:: All the (non)-lazy attributes
492
+ #
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
499
+ end
500
+
501
+ # Mass assign of attributes
502
+ #
503
+ # ==== Parameters
504
+ # value_hash <Hash[<Symbol>]>::
505
+ #
506
+ # --
507
+ # @api public
508
+ def attributes=(values_hash)
509
+ values_hash.each do |name, value|
510
+ name = name.to_s.sub(/\?\z/, '')
511
+
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
518
+ end
519
+
520
+ # Updates attributes and saves model
521
+ #
522
+ # ==== Parameters
523
+ # attributes<Hash> Attributes to be updated
524
+ # keys<Symbol, String, Array> keys of Hash to update (others won't be updated)
525
+ #
526
+ # ==== Returns
527
+ # <TrueClass, FalseClass> if model got saved or not
528
+ #
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}"
535
+ end
536
+ loop_thru = update_only.empty? ? hash.keys : update_only
537
+ loop_thru.each { |attr| send("#{attr}=", hash[attr]) }
538
+ save
539
+ end
540
+
541
+ # TODO: add docs
542
+ def to_query(query = {})
543
+ model.to_query(repository, key, query) unless new_record?
544
+ end
545
+
546
+ # TODO: add docs
547
+ # @api private
548
+ def _dump(*)
549
+ ivars = {}
550
+
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
556
+
557
+ # dump ivars used internally
558
+ %w[ @new_record @original_values @readonly @repository ].each do |name|
559
+ ivars[name] = instance_variable_get(name)
560
+ end
561
+
562
+ Marshal.dump(ivars)
563
+ end
564
+
565
+ protected
566
+
567
+ def properties
568
+ model.properties(repository.name)
569
+ end
570
+
571
+ def key_properties
572
+ model.key(repository.name)
573
+ end
574
+
575
+ def relationships
576
+ model.relationships(repository.name)
577
+ end
578
+
579
+ # Needs to be a protected method so that it is hookable
580
+ def create
581
+ # 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? }
583
+ # set defaults for new resource
584
+ properties.each do |property|
585
+ next if attribute_loaded?(property.name)
586
+ property.set(self, property.default_for(self))
587
+ end
588
+
589
+ return false unless repository.create([ self ]) == 1
590
+
591
+ @repository = repository
592
+ @new_record = false
593
+
594
+ repository.identity_map(model).set(key, self)
595
+
596
+ true
597
+ end
598
+
599
+ # Needs to be a protected method so that it is hookable
600
+ def update
601
+ dirty_attributes = self.dirty_attributes
602
+ return true if dirty_attributes.empty?
603
+ repository.update(dirty_attributes, to_query) == 1
604
+ end
605
+
606
+ private
607
+
608
+ def initialize(attributes = {}) # :nodoc:
609
+ assert_valid_model
610
+ self.attributes = attributes
611
+ end
612
+
613
+ def assert_valid_model # :nodoc:
614
+ return if self.class._valid_model
615
+ properties = self.properties
616
+
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
627
+
628
+ # TODO document
629
+ # @api semipublic
630
+ def attribute_get!(name)
631
+ properties[name].get!(self)
632
+ end
633
+
634
+ # TODO document
635
+ # @api semipublic
636
+ def attribute_set!(name, value)
637
+ properties[name].set!(self, value)
638
+ end
639
+
640
+ def lazy_load(name)
641
+ reload_attributes(*properties.lazy_load_context(name) - loaded_attributes)
642
+ end
643
+
644
+ def child_associations
645
+ @child_associations ||= []
646
+ end
647
+
648
+ def parent_associations
649
+ @parent_associations ||= []
650
+ end
651
+
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) }
666
+ end
667
+ end # module Transaction
668
+
669
+ include Transaction
670
+ end # module Resource
671
+ end # module DataMapper