dm-core 0.9.2

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