dm-core 0.9.2

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