rpbertp13-dm-core 0.9.11.1

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 (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 +52 -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 +32 -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 +138 -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 +221 -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 +252 -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 +60 -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 +469 -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