sam-dm-core 0.9.6

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 (126) hide show
  1. data/.autotest +26 -0
  2. data/CONTRIBUTING +51 -0
  3. data/FAQ +92 -0
  4. data/History.txt +145 -0
  5. data/MIT-LICENSE +22 -0
  6. data/Manifest.txt +125 -0
  7. data/QUICKLINKS +12 -0
  8. data/README.txt +143 -0
  9. data/Rakefile +30 -0
  10. data/SPECS +63 -0
  11. data/TODO +1 -0
  12. data/lib/dm-core.rb +224 -0
  13. data/lib/dm-core/adapters.rb +4 -0
  14. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  15. data/lib/dm-core/adapters/data_objects_adapter.rb +707 -0
  16. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +188 -0
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  19. data/lib/dm-core/associations.rb +199 -0
  20. data/lib/dm-core/associations/many_to_many.rb +147 -0
  21. data/lib/dm-core/associations/many_to_one.rb +107 -0
  22. data/lib/dm-core/associations/one_to_many.rb +309 -0
  23. data/lib/dm-core/associations/one_to_one.rb +61 -0
  24. data/lib/dm-core/associations/relationship.rb +218 -0
  25. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  26. data/lib/dm-core/auto_migrations.rb +113 -0
  27. data/lib/dm-core/collection.rb +638 -0
  28. data/lib/dm-core/dependency_queue.rb +31 -0
  29. data/lib/dm-core/hook.rb +11 -0
  30. data/lib/dm-core/identity_map.rb +45 -0
  31. data/lib/dm-core/is.rb +16 -0
  32. data/lib/dm-core/logger.rb +232 -0
  33. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  34. data/lib/dm-core/migrator.rb +29 -0
  35. data/lib/dm-core/model.rb +471 -0
  36. data/lib/dm-core/naming_conventions.rb +84 -0
  37. data/lib/dm-core/property.rb +673 -0
  38. data/lib/dm-core/property_set.rb +162 -0
  39. data/lib/dm-core/query.rb +625 -0
  40. data/lib/dm-core/repository.rb +159 -0
  41. data/lib/dm-core/resource.rb +637 -0
  42. data/lib/dm-core/scope.rb +58 -0
  43. data/lib/dm-core/support.rb +7 -0
  44. data/lib/dm-core/support/array.rb +13 -0
  45. data/lib/dm-core/support/assertions.rb +8 -0
  46. data/lib/dm-core/support/errors.rb +23 -0
  47. data/lib/dm-core/support/kernel.rb +7 -0
  48. data/lib/dm-core/support/symbol.rb +41 -0
  49. data/lib/dm-core/transaction.rb +267 -0
  50. data/lib/dm-core/type.rb +160 -0
  51. data/lib/dm-core/type_map.rb +80 -0
  52. data/lib/dm-core/types.rb +19 -0
  53. data/lib/dm-core/types/boolean.rb +7 -0
  54. data/lib/dm-core/types/discriminator.rb +34 -0
  55. data/lib/dm-core/types/object.rb +24 -0
  56. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  57. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  58. data/lib/dm-core/types/serial.rb +9 -0
  59. data/lib/dm-core/types/text.rb +10 -0
  60. data/lib/dm-core/version.rb +3 -0
  61. data/script/all +5 -0
  62. data/script/performance.rb +203 -0
  63. data/script/profile.rb +87 -0
  64. data/spec/integration/association_spec.rb +1371 -0
  65. data/spec/integration/association_through_spec.rb +203 -0
  66. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  67. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  68. data/spec/integration/associations/one_to_many_spec.rb +151 -0
  69. data/spec/integration/auto_migrations_spec.rb +398 -0
  70. data/spec/integration/collection_spec.rb +1069 -0
  71. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  72. data/spec/integration/dependency_queue_spec.rb +58 -0
  73. data/spec/integration/model_spec.rb +127 -0
  74. data/spec/integration/mysql_adapter_spec.rb +85 -0
  75. data/spec/integration/postgres_adapter_spec.rb +731 -0
  76. data/spec/integration/property_spec.rb +233 -0
  77. data/spec/integration/query_spec.rb +506 -0
  78. data/spec/integration/repository_spec.rb +57 -0
  79. data/spec/integration/resource_spec.rb +475 -0
  80. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  81. data/spec/integration/sti_spec.rb +208 -0
  82. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  83. data/spec/integration/transaction_spec.rb +75 -0
  84. data/spec/integration/type_spec.rb +271 -0
  85. data/spec/lib/logging_helper.rb +18 -0
  86. data/spec/lib/mock_adapter.rb +27 -0
  87. data/spec/lib/model_loader.rb +91 -0
  88. data/spec/lib/publicize_methods.rb +28 -0
  89. data/spec/models/vehicles.rb +34 -0
  90. data/spec/models/zoo.rb +47 -0
  91. data/spec/spec.opts +3 -0
  92. data/spec/spec_helper.rb +86 -0
  93. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  94. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  95. data/spec/unit/adapters/data_objects_adapter_spec.rb +628 -0
  96. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  97. data/spec/unit/associations/many_to_many_spec.rb +17 -0
  98. data/spec/unit/associations/many_to_one_spec.rb +152 -0
  99. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  100. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  101. data/spec/unit/associations/relationship_spec.rb +71 -0
  102. data/spec/unit/associations_spec.rb +242 -0
  103. data/spec/unit/auto_migrations_spec.rb +111 -0
  104. data/spec/unit/collection_spec.rb +182 -0
  105. data/spec/unit/data_mapper_spec.rb +35 -0
  106. data/spec/unit/identity_map_spec.rb +126 -0
  107. data/spec/unit/is_spec.rb +80 -0
  108. data/spec/unit/migrator_spec.rb +33 -0
  109. data/spec/unit/model_spec.rb +339 -0
  110. data/spec/unit/naming_conventions_spec.rb +36 -0
  111. data/spec/unit/property_set_spec.rb +83 -0
  112. data/spec/unit/property_spec.rb +753 -0
  113. data/spec/unit/query_spec.rb +530 -0
  114. data/spec/unit/repository_spec.rb +93 -0
  115. data/spec/unit/resource_spec.rb +626 -0
  116. data/spec/unit/scope_spec.rb +142 -0
  117. data/spec/unit/transaction_spec.rb +493 -0
  118. data/spec/unit/type_map_spec.rb +114 -0
  119. data/spec/unit/type_spec.rb +119 -0
  120. data/tasks/ci.rb +68 -0
  121. data/tasks/dm.rb +63 -0
  122. data/tasks/doc.rb +20 -0
  123. data/tasks/gemspec.rb +23 -0
  124. data/tasks/hoe.rb +46 -0
  125. data/tasks/install.rb +20 -0
  126. metadata +216 -0
@@ -0,0 +1,159 @@
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]
37
+ end
38
+
39
+ # TODO: spec this
40
+ def scope(&block)
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
+ private
95
+
96
+ def initialize(name)
97
+ assert_kind_of 'name', name, Symbol
98
+
99
+ @name = name
100
+ @identity_maps = Hash.new { |h,model| h[model] = IdentityMap.new }
101
+ end
102
+
103
+ # TODO: move to dm-more/dm-migrations
104
+ module Migration
105
+ # TODO: move to dm-more/dm-migrations
106
+ def map(*args)
107
+ type_map.map(*args)
108
+ end
109
+
110
+ # TODO: move to dm-more/dm-migrations
111
+ def type_map
112
+ @type_map ||= TypeMap.new(adapter.class.type_map)
113
+ end
114
+
115
+ ##
116
+ #
117
+ # @return <True, False> whether or not the data-store exists for this repo
118
+ #
119
+ # TODO: move to dm-more/dm-migrations
120
+ def storage_exists?(storage_name)
121
+ adapter.storage_exists?(storage_name)
122
+ end
123
+
124
+ # TODO: move to dm-more/dm-migrations
125
+ def migrate!
126
+ Migrator.migrate(name)
127
+ end
128
+
129
+ # TODO: move to dm-more/dm-migrations
130
+ def auto_migrate!
131
+ AutoMigrator.auto_migrate(name)
132
+ end
133
+
134
+ # TODO: move to dm-more/dm-migrations
135
+ def auto_upgrade!
136
+ AutoMigrator.auto_upgrade(name)
137
+ end
138
+ end
139
+
140
+ include Migration
141
+
142
+ # TODO: move to dm-more/dm-transactions
143
+ module Transaction
144
+ ##
145
+ # Produce a new Transaction for this Repository
146
+ #
147
+ #
148
+ # @return <DataMapper::Adapters::Transaction> a new Transaction (in state
149
+ # :none) that can be used to execute code #with_transaction
150
+ #
151
+ # TODO: move to dm-more/dm-transactions
152
+ def transaction
153
+ DataMapper::Transaction.new(self)
154
+ end
155
+ end
156
+
157
+ include Transaction
158
+ end # class Repository
159
+ end # module DataMapper
@@ -0,0 +1,637 @@
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
+ # ==== Parameters
153
+ # other<Object>:: Object to compare to
154
+ #
155
+ # ==== Returns
156
+ # <True>:: the outcome of the comparison as a boolean
157
+ #
158
+ # -
159
+ # @api public
160
+ def eql?(other)
161
+ return true if object_id == other.object_id
162
+ return false unless other.kind_of?(model)
163
+ return true if repository == other.repository && key == other.key
164
+
165
+ properties.each do |property|
166
+ return false if property.get!(self) != property.get!(other)
167
+ end
168
+
169
+ true
170
+ end
171
+
172
+ alias == eql?
173
+
174
+ # Computes a hash for the resource
175
+ #
176
+ # ==== Returns
177
+ # <Integer>:: the hash value of the resource
178
+ #
179
+ # -
180
+ # @api public
181
+ def hash
182
+ model.hash + key.hash
183
+ end
184
+
185
+ # Inspection of the class name and the attributes
186
+ #
187
+ # ==== Returns
188
+ # <String>:: with the class name, attributes with their values
189
+ #
190
+ # ==== Example
191
+ #
192
+ # >> Foo.new
193
+ # => #<Foo name=nil updated_at=nil created_at=nil id=nil>
194
+ #
195
+ # -
196
+ # @api public
197
+ def inspect
198
+ attrs = []
199
+
200
+ properties.each do |property|
201
+ value = if property.lazy? && !attribute_loaded?(property.name) && !new_record?
202
+ '<not loaded>'
203
+ else
204
+ send(property.getter).inspect
205
+ end
206
+
207
+ attrs << "#{property.name}=#{value}"
208
+ end
209
+
210
+ "#<#{model.name} #{attrs * ' '}>"
211
+ end
212
+
213
+ # TODO docs
214
+ def pretty_print(pp)
215
+ pp.group(1, "#<#{model.name}", ">") do
216
+ pp.breakable
217
+ pp.seplist(attributes.to_a) do |k_v|
218
+ pp.text k_v[0].to_s
219
+ pp.text " = "
220
+ pp.pp k_v[1]
221
+ end
222
+ end
223
+ end
224
+
225
+ ##
226
+ #
227
+ # ==== Returns
228
+ # <Repository>:: the respository this resource belongs to in the context of a collection OR in the class's context
229
+ #
230
+ # @api public
231
+ def repository
232
+ @repository || model.repository
233
+ end
234
+
235
+ # default id method to return the resource id when there is a
236
+ # single key, and the model was defined with a primary key named
237
+ # something other than id
238
+ #
239
+ # ==== Returns
240
+ # <Array[Key], Key> key or keys
241
+ #
242
+ # --
243
+ # @api public
244
+ def id
245
+ key = self.key
246
+ key.first if key.size == 1
247
+ end
248
+
249
+ def key
250
+ key_properties.map do |property|
251
+ original_values[property.name] || property.get!(self)
252
+ end
253
+ end
254
+
255
+ def readonly!
256
+ @readonly = true
257
+ end
258
+
259
+ def readonly?
260
+ @readonly == true
261
+ end
262
+
263
+ # save the instance to the data-store
264
+ #
265
+ # ==== Returns
266
+ # <True, False>:: results of the save
267
+ #
268
+ # @see DataMapper::Repository#save
269
+ #
270
+ # --
271
+ # #public
272
+ def save(context = :default)
273
+ # Takes a context, but does nothing with it. This is to maintain the
274
+ # same API through out all of dm-more. dm-validations requires a
275
+ # context to be passed
276
+
277
+ associations_saved = false
278
+ child_associations.each { |a| associations_saved |= a.save }
279
+
280
+ saved = if dirty? || (new_record? && key_properties.any? { |p| p.serial? })
281
+ new_record? ? create : update
282
+ end
283
+
284
+ if saved
285
+ original_values.clear
286
+ end
287
+
288
+ parent_associations.each { |a| associations_saved |= a.save }
289
+
290
+ # We should return true if the model (or any of its associations)
291
+ # were saved.
292
+ (saved | associations_saved) == true
293
+ end
294
+
295
+ # destroy the instance, remove it from the repository
296
+ #
297
+ # ==== Returns
298
+ # <True, False>:: results of the destruction
299
+ #
300
+ # --
301
+ # @api public
302
+ def destroy
303
+ return false if new_record?
304
+ return false unless repository.delete(to_query)
305
+
306
+ @new_record = true
307
+ repository.identity_map(model).delete(key)
308
+ original_values.clear
309
+
310
+ properties.each do |property|
311
+ # We'll set the original value to nil as if we had a new record
312
+ original_values[property.name] = nil if attribute_loaded?(property.name)
313
+ end
314
+
315
+ true
316
+ end
317
+
318
+ # Checks if the attribute has been loaded
319
+ #
320
+ # ==== Example
321
+ #
322
+ # class Foo
323
+ # include DataMapper::Resource
324
+ # property :name, String
325
+ # property :description, Text, :lazy => false
326
+ # end
327
+ #
328
+ # Foo.new.attribute_loaded?(:description) # will return false
329
+ #
330
+ # --
331
+ # @api public
332
+ def attribute_loaded?(name)
333
+ instance_variable_defined?(properties[name].instance_variable_name)
334
+ end
335
+
336
+ # fetches all the names of the attributes that have been loaded,
337
+ # even if they are lazy but have been called
338
+ #
339
+ # ==== Returns
340
+ # Array[<Symbol>]:: names of attributes that have been loaded
341
+ #
342
+ # ==== Example
343
+ #
344
+ # class Foo
345
+ # include DataMapper::Resource
346
+ # property :name, String
347
+ # property :description, Text, :lazy => false
348
+ # end
349
+ #
350
+ # Foo.new.loaded_attributes # returns [:name]
351
+ #
352
+ # --
353
+ # @api public
354
+ def loaded_attributes
355
+ properties.map{|p| p.name if attribute_loaded?(p.name)}.compact
356
+ end
357
+
358
+ # set of original values of properties
359
+ #
360
+ # ==== Returns
361
+ # Hash:: original values of properties
362
+ #
363
+ # --
364
+ # @api public
365
+ def original_values
366
+ @original_values ||= {}
367
+ end
368
+
369
+ # Hash of attributes that have been marked dirty
370
+ #
371
+ # ==== Returns
372
+ # Hash:: attributes that have been marked dirty
373
+ #
374
+ # --
375
+ # @api private
376
+ def dirty_attributes
377
+ dirty_attributes = {}
378
+ properties = self.properties
379
+
380
+ original_values.each do |name, old_value|
381
+ property = properties[name]
382
+ new_value = property.get!(self)
383
+
384
+ dirty = case property.track
385
+ when :hash then old_value != new_value.hash
386
+ else
387
+ property.value(old_value) != property.value(new_value)
388
+ end
389
+
390
+ if dirty
391
+ property.hash
392
+ dirty_attributes[property] = property.value(new_value)
393
+ end
394
+ end
395
+
396
+ dirty_attributes
397
+ end
398
+
399
+ # Checks if the class is dirty
400
+ #
401
+ # ==== Returns
402
+ # True:: returns if class is dirty
403
+ #
404
+ # --
405
+ # @api public
406
+ def dirty?
407
+ dirty_attributes.any?
408
+ end
409
+
410
+ # Checks if the attribute is dirty
411
+ #
412
+ # ==== Parameters
413
+ # name<Symbol>:: name of attribute
414
+ #
415
+ # ==== Returns
416
+ # True:: returns if attribute is dirty
417
+ #
418
+ # --
419
+ # @api public
420
+ def attribute_dirty?(name)
421
+ dirty_attributes.has_key?(properties[name])
422
+ end
423
+
424
+ def collection
425
+ @collection ||= if query = to_query
426
+ Collection.new(query) { |c| c << self }
427
+ end
428
+ end
429
+
430
+ # Reload association and all child association
431
+ #
432
+ # ==== Returns
433
+ # self:: returns the class itself
434
+ #
435
+ # --
436
+ # @api public
437
+ def reload
438
+ unless new_record?
439
+ reload_attributes(*loaded_attributes)
440
+ (parent_associations + child_associations).each { |association| association.reload }
441
+ end
442
+
443
+ self
444
+ end
445
+
446
+ # Reload specific attributes
447
+ #
448
+ # ==== Parameters
449
+ # *attributes<Array[<Symbol>]>:: name of attribute
450
+ #
451
+ # ==== Returns
452
+ # self:: returns the class itself
453
+ #
454
+ # --
455
+ # @api public
456
+ def reload_attributes(*attributes)
457
+ unless attributes.empty? || new_record?
458
+ collection.reload(:fields => attributes)
459
+ end
460
+
461
+ self
462
+ end
463
+
464
+ # Checks if the model has been saved
465
+ #
466
+ # ==== Returns
467
+ # True:: status if the model is new
468
+ #
469
+ # --
470
+ # @api public
471
+ def new_record?
472
+ !defined?(@new_record) || @new_record
473
+ end
474
+
475
+ # all the attributes of the model
476
+ #
477
+ # ==== Returns
478
+ # Hash[<Symbol>]:: All the (non)-lazy attributes
479
+ #
480
+ # --
481
+ # @api public
482
+ def attributes
483
+ properties.map do |p|
484
+ [p.name, send(p.getter)] if p.reader_visibility == :public
485
+ end.compact.to_hash
486
+ end
487
+
488
+ # Mass assign of attributes
489
+ #
490
+ # ==== Parameters
491
+ # value_hash <Hash[<Symbol>]>::
492
+ #
493
+ # --
494
+ # @api public
495
+ def attributes=(values_hash)
496
+ values_hash.each_pair do |k,v|
497
+ setter = "#{k.to_s.sub(/\?\z/, '')}="
498
+
499
+ if respond_to?(setter)
500
+ send(setter, v)
501
+ else
502
+ raise NameError, "#{setter} is not a public property"
503
+ end
504
+ end
505
+ end
506
+
507
+ # Updates attributes and saves model
508
+ #
509
+ # ==== Parameters
510
+ # attributes<Hash> Attributes to be updated
511
+ # keys<Symbol, String, Array> keys of Hash to update (others won't be updated)
512
+ #
513
+ # ==== Returns
514
+ # <TrueClass, FalseClass> if model got saved or not
515
+ #
516
+ #-
517
+ # @api public
518
+ def update_attributes(hash, *update_only)
519
+ unless hash.is_a?(Hash)
520
+ raise ArgumentError, "Expecting the first parameter of " +
521
+ "update_attributes to be a hash; got #{hash.inspect}"
522
+ end
523
+ loop_thru = update_only.empty? ? hash.keys : update_only
524
+ loop_thru.each { |attr| send("#{attr}=", hash[attr]) }
525
+ save
526
+ end
527
+
528
+ # TODO: add docs
529
+ def to_query(query = {})
530
+ model.to_query(repository, key, query) unless new_record?
531
+ end
532
+
533
+ protected
534
+
535
+ def properties
536
+ model.properties(repository.name)
537
+ end
538
+
539
+ def key_properties
540
+ model.key(repository.name)
541
+ end
542
+
543
+ def relationships
544
+ model.relationships(repository.name)
545
+ end
546
+
547
+ # Needs to be a protected method so that it is hookable
548
+ def create
549
+ # set defaults for new resource
550
+ properties.each do |property|
551
+ next if attribute_loaded?(property.name)
552
+ property.set(self, property.default_for(self))
553
+ end
554
+
555
+ return false unless repository.create([ self ]) == 1
556
+
557
+ @repository = repository
558
+ @new_record = false
559
+
560
+ repository.identity_map(model).set(key, self)
561
+
562
+ true
563
+ end
564
+
565
+ # Needs to be a protected method so that it is hookable
566
+ def update
567
+ dirty_attributes = self.dirty_attributes
568
+ return true if dirty_attributes.empty?
569
+ repository.update(dirty_attributes, to_query) == 1
570
+ end
571
+
572
+ private
573
+
574
+ def initialize(attributes = {}) # :nodoc:
575
+ assert_valid_model
576
+ self.attributes = attributes
577
+ end
578
+
579
+ def assert_valid_model # :nodoc:
580
+ return if self.class._valid_model
581
+ properties = self.properties
582
+
583
+ if properties.empty? && relationships.empty?
584
+ raise IncompleteResourceError, "#{model.name} must have at least one property or relationship to be initialized."
585
+ end
586
+
587
+ if properties.key.empty?
588
+ raise IncompleteResourceError, "#{model.name} must have a key."
589
+ end
590
+
591
+ class << self; @_valid_model = true; end
592
+ end
593
+
594
+ # TODO document
595
+ # @api semipublic
596
+ def attribute_get!(name)
597
+ properties[name].get!(self)
598
+ end
599
+
600
+ # TODO document
601
+ # @api semipublic
602
+ def attribute_set!(name, value)
603
+ properties[name].set!(self, value)
604
+ end
605
+
606
+ def lazy_load(name)
607
+ reload_attributes(*properties.lazy_load_context(name) - loaded_attributes)
608
+ end
609
+
610
+ def child_associations
611
+ @child_associations ||= []
612
+ end
613
+
614
+ def parent_associations
615
+ @parent_associations ||= []
616
+ end
617
+
618
+ # TODO: move to dm-more/dm-transactions
619
+ module Transaction
620
+ # Produce a new Transaction for the class of this Resource
621
+ #
622
+ # ==== Returns
623
+ # <DataMapper::Adapters::Transaction>::
624
+ # a new DataMapper::Adapters::Transaction with all DataMapper::Repositories
625
+ # of the class of this DataMapper::Resource added.
626
+ #-
627
+ # @api public
628
+ #
629
+ # TODO: move to dm-more/dm-transactions
630
+ def transaction(&block)
631
+ model.transaction(&block)
632
+ end
633
+ end # module Transaction
634
+
635
+ include Transaction
636
+ end # module Resource
637
+ end # module DataMapper