ghost_dm-core 1.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (254) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +35 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +65 -0
  6. data/LICENSE +20 -0
  7. data/README.md +269 -0
  8. data/Rakefile +4 -0
  9. data/dm-core.gemspec +24 -0
  10. data/lib/dm-core.rb +292 -0
  11. data/lib/dm-core/adapters.rb +222 -0
  12. data/lib/dm-core/adapters/abstract_adapter.rb +237 -0
  13. data/lib/dm-core/adapters/in_memory_adapter.rb +113 -0
  14. data/lib/dm-core/associations/many_to_many.rb +499 -0
  15. data/lib/dm-core/associations/many_to_one.rb +290 -0
  16. data/lib/dm-core/associations/one_to_many.rb +348 -0
  17. data/lib/dm-core/associations/one_to_one.rb +86 -0
  18. data/lib/dm-core/associations/relationship.rb +663 -0
  19. data/lib/dm-core/backwards.rb +13 -0
  20. data/lib/dm-core/collection.rb +1515 -0
  21. data/lib/dm-core/core_ext/kernel.rb +23 -0
  22. data/lib/dm-core/core_ext/pathname.rb +6 -0
  23. data/lib/dm-core/core_ext/symbol.rb +10 -0
  24. data/lib/dm-core/identity_map.rb +7 -0
  25. data/lib/dm-core/model.rb +874 -0
  26. data/lib/dm-core/model/hook.rb +103 -0
  27. data/lib/dm-core/model/is.rb +32 -0
  28. data/lib/dm-core/model/property.rb +249 -0
  29. data/lib/dm-core/model/relationship.rb +378 -0
  30. data/lib/dm-core/model/scope.rb +89 -0
  31. data/lib/dm-core/property.rb +866 -0
  32. data/lib/dm-core/property/binary.rb +21 -0
  33. data/lib/dm-core/property/boolean.rb +20 -0
  34. data/lib/dm-core/property/class.rb +17 -0
  35. data/lib/dm-core/property/date.rb +10 -0
  36. data/lib/dm-core/property/date_time.rb +10 -0
  37. data/lib/dm-core/property/decimal.rb +36 -0
  38. data/lib/dm-core/property/discriminator.rb +44 -0
  39. data/lib/dm-core/property/float.rb +16 -0
  40. data/lib/dm-core/property/integer.rb +22 -0
  41. data/lib/dm-core/property/invalid_value_error.rb +22 -0
  42. data/lib/dm-core/property/lookup.rb +27 -0
  43. data/lib/dm-core/property/numeric.rb +38 -0
  44. data/lib/dm-core/property/object.rb +34 -0
  45. data/lib/dm-core/property/serial.rb +14 -0
  46. data/lib/dm-core/property/string.rb +38 -0
  47. data/lib/dm-core/property/text.rb +9 -0
  48. data/lib/dm-core/property/time.rb +10 -0
  49. data/lib/dm-core/property_set.rb +177 -0
  50. data/lib/dm-core/query.rb +1366 -0
  51. data/lib/dm-core/query/conditions/comparison.rb +911 -0
  52. data/lib/dm-core/query/conditions/operation.rb +721 -0
  53. data/lib/dm-core/query/direction.rb +36 -0
  54. data/lib/dm-core/query/operator.rb +35 -0
  55. data/lib/dm-core/query/path.rb +114 -0
  56. data/lib/dm-core/query/sort.rb +39 -0
  57. data/lib/dm-core/relationship_set.rb +72 -0
  58. data/lib/dm-core/repository.rb +226 -0
  59. data/lib/dm-core/resource.rb +1214 -0
  60. data/lib/dm-core/resource/persistence_state.rb +75 -0
  61. data/lib/dm-core/resource/persistence_state/clean.rb +40 -0
  62. data/lib/dm-core/resource/persistence_state/deleted.rb +30 -0
  63. data/lib/dm-core/resource/persistence_state/dirty.rb +96 -0
  64. data/lib/dm-core/resource/persistence_state/immutable.rb +34 -0
  65. data/lib/dm-core/resource/persistence_state/persisted.rb +29 -0
  66. data/lib/dm-core/resource/persistence_state/transient.rb +80 -0
  67. data/lib/dm-core/spec/lib/adapter_helpers.rb +64 -0
  68. data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
  69. data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
  70. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  71. data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
  72. data/lib/dm-core/spec/setup.rb +174 -0
  73. data/lib/dm-core/spec/shared/adapter_spec.rb +341 -0
  74. data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
  75. data/lib/dm-core/spec/shared/resource_spec.rb +1232 -0
  76. data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
  77. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +176 -0
  78. data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
  79. data/lib/dm-core/support/assertions.rb +8 -0
  80. data/lib/dm-core/support/chainable.rb +18 -0
  81. data/lib/dm-core/support/deprecate.rb +12 -0
  82. data/lib/dm-core/support/descendant_set.rb +89 -0
  83. data/lib/dm-core/support/equalizer.rb +48 -0
  84. data/lib/dm-core/support/ext/array.rb +22 -0
  85. data/lib/dm-core/support/ext/blank.rb +25 -0
  86. data/lib/dm-core/support/ext/hash.rb +67 -0
  87. data/lib/dm-core/support/ext/module.rb +47 -0
  88. data/lib/dm-core/support/ext/object.rb +57 -0
  89. data/lib/dm-core/support/ext/string.rb +24 -0
  90. data/lib/dm-core/support/ext/try_dup.rb +12 -0
  91. data/lib/dm-core/support/hook.rb +405 -0
  92. data/lib/dm-core/support/inflections.rb +60 -0
  93. data/lib/dm-core/support/inflector/inflections.rb +211 -0
  94. data/lib/dm-core/support/inflector/methods.rb +151 -0
  95. data/lib/dm-core/support/lazy_array.rb +451 -0
  96. data/lib/dm-core/support/local_object_space.rb +13 -0
  97. data/lib/dm-core/support/logger.rb +201 -0
  98. data/lib/dm-core/support/mash.rb +176 -0
  99. data/lib/dm-core/support/naming_conventions.rb +90 -0
  100. data/lib/dm-core/support/ordered_set.rb +380 -0
  101. data/lib/dm-core/support/subject.rb +33 -0
  102. data/lib/dm-core/support/subject_set.rb +250 -0
  103. data/lib/dm-core/version.rb +3 -0
  104. data/script/performance.rb +275 -0
  105. data/script/profile.rb +218 -0
  106. data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
  107. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +68 -0
  108. data/spec/public/associations/many_to_many_spec.rb +197 -0
  109. data/spec/public/associations/many_to_one_spec.rb +83 -0
  110. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
  111. data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
  112. data/spec/public/associations/one_to_many_spec.rb +81 -0
  113. data/spec/public/associations/one_to_one_spec.rb +176 -0
  114. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
  115. data/spec/public/collection_spec.rb +69 -0
  116. data/spec/public/finalize_spec.rb +76 -0
  117. data/spec/public/model/hook_spec.rb +246 -0
  118. data/spec/public/model/property_spec.rb +88 -0
  119. data/spec/public/model/relationship_spec.rb +1040 -0
  120. data/spec/public/model_spec.rb +462 -0
  121. data/spec/public/property/binary_spec.rb +41 -0
  122. data/spec/public/property/boolean_spec.rb +22 -0
  123. data/spec/public/property/class_spec.rb +28 -0
  124. data/spec/public/property/date_spec.rb +22 -0
  125. data/spec/public/property/date_time_spec.rb +22 -0
  126. data/spec/public/property/decimal_spec.rb +23 -0
  127. data/spec/public/property/discriminator_spec.rb +135 -0
  128. data/spec/public/property/float_spec.rb +22 -0
  129. data/spec/public/property/integer_spec.rb +22 -0
  130. data/spec/public/property/object_spec.rb +107 -0
  131. data/spec/public/property/serial_spec.rb +22 -0
  132. data/spec/public/property/string_spec.rb +22 -0
  133. data/spec/public/property/text_spec.rb +63 -0
  134. data/spec/public/property/time_spec.rb +22 -0
  135. data/spec/public/property_spec.rb +341 -0
  136. data/spec/public/resource_spec.rb +288 -0
  137. data/spec/public/sel_spec.rb +53 -0
  138. data/spec/public/setup_spec.rb +145 -0
  139. data/spec/public/shared/association_collection_shared_spec.rb +309 -0
  140. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  141. data/spec/public/shared/collection_shared_spec.rb +1667 -0
  142. data/spec/public/shared/finder_shared_spec.rb +1629 -0
  143. data/spec/rcov.opts +6 -0
  144. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  145. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
  146. data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
  147. data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
  148. data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
  149. data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
  150. data/spec/semipublic/associations/relationship_spec.rb +200 -0
  151. data/spec/semipublic/associations_spec.rb +177 -0
  152. data/spec/semipublic/collection_spec.rb +110 -0
  153. data/spec/semipublic/model_spec.rb +96 -0
  154. data/spec/semipublic/property/binary_spec.rb +13 -0
  155. data/spec/semipublic/property/boolean_spec.rb +47 -0
  156. data/spec/semipublic/property/class_spec.rb +33 -0
  157. data/spec/semipublic/property/date_spec.rb +43 -0
  158. data/spec/semipublic/property/date_time_spec.rb +46 -0
  159. data/spec/semipublic/property/decimal_spec.rb +83 -0
  160. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  161. data/spec/semipublic/property/float_spec.rb +82 -0
  162. data/spec/semipublic/property/integer_spec.rb +82 -0
  163. data/spec/semipublic/property/lookup_spec.rb +29 -0
  164. data/spec/semipublic/property/serial_spec.rb +13 -0
  165. data/spec/semipublic/property/string_spec.rb +13 -0
  166. data/spec/semipublic/property/text_spec.rb +31 -0
  167. data/spec/semipublic/property/time_spec.rb +50 -0
  168. data/spec/semipublic/property_spec.rb +114 -0
  169. data/spec/semipublic/query/conditions/comparison_spec.rb +1501 -0
  170. data/spec/semipublic/query/conditions/operation_spec.rb +1294 -0
  171. data/spec/semipublic/query/path_spec.rb +471 -0
  172. data/spec/semipublic/query_spec.rb +3682 -0
  173. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  174. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  175. data/spec/semipublic/resource/state/dirty_spec.rb +162 -0
  176. data/spec/semipublic/resource/state/immutable_spec.rb +105 -0
  177. data/spec/semipublic/resource/state/transient_spec.rb +162 -0
  178. data/spec/semipublic/resource/state_spec.rb +230 -0
  179. data/spec/semipublic/resource_spec.rb +23 -0
  180. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  181. data/spec/semipublic/shared/resource_shared_spec.rb +199 -0
  182. data/spec/semipublic/shared/resource_state_shared_spec.rb +79 -0
  183. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  184. data/spec/spec.opts +5 -0
  185. data/spec/spec_helper.rb +38 -0
  186. data/spec/support/core_ext/hash.rb +10 -0
  187. data/spec/support/core_ext/inheritable_attributes.rb +46 -0
  188. data/spec/support/properties/huge_integer.rb +17 -0
  189. data/spec/unit/array_spec.rb +23 -0
  190. data/spec/unit/blank_spec.rb +73 -0
  191. data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
  192. data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
  193. data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
  194. data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
  195. data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
  196. data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
  197. data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
  198. data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
  199. data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
  200. data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
  201. data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
  202. data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
  203. data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
  204. data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
  205. data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
  206. data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
  207. data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
  208. data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
  209. data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
  210. data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
  211. data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
  212. data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
  213. data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
  214. data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
  215. data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
  216. data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
  217. data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
  218. data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
  219. data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
  220. data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
  221. data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
  222. data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
  223. data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
  224. data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
  225. data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
  226. data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
  227. data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
  228. data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
  229. data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
  230. data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
  231. data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
  232. data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
  233. data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
  234. data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
  235. data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
  236. data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
  237. data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
  238. data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
  239. data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
  240. data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
  241. data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
  242. data/spec/unit/hash_spec.rb +28 -0
  243. data/spec/unit/hook_spec.rb +1235 -0
  244. data/spec/unit/inflections_spec.rb +16 -0
  245. data/spec/unit/lazy_array_spec.rb +1949 -0
  246. data/spec/unit/mash_spec.rb +312 -0
  247. data/spec/unit/module_spec.rb +71 -0
  248. data/spec/unit/object_spec.rb +38 -0
  249. data/spec/unit/try_dup_spec.rb +46 -0
  250. data/tasks/ci.rake +1 -0
  251. data/tasks/spec.rake +38 -0
  252. data/tasks/yard.rake +9 -0
  253. data/tasks/yardstick.rake +19 -0
  254. metadata +365 -0
@@ -0,0 +1,237 @@
1
+ module DataMapper
2
+ module Adapters
3
+ # Specific adapters extend this class and implement
4
+ # methods for creating, reading, updating and deleting records.
5
+ #
6
+ # Adapters may only implement method for reading or (less common case)
7
+ # writing. Read only adapter may be useful when one needs to work
8
+ # with legacy data that should not be changed or web services that
9
+ # only provide read access to data (from Wordnet and Medline to
10
+ # Atom and RSS syndication feeds)
11
+ #
12
+ # Note that in case of adapters to relational databases it makes
13
+ # sense to inherit from DataObjectsAdapter class.
14
+ class AbstractAdapter
15
+ include DataMapper::Assertions
16
+ extend DataMapper::Assertions
17
+ extend Equalizer
18
+
19
+ equalize :name, :options, :resource_naming_convention, :field_naming_convention
20
+
21
+ # @api semipublic
22
+ def self.descendants
23
+ @descendants ||= DescendantSet.new
24
+ end
25
+
26
+ # @api private
27
+ def self.inherited(descendant)
28
+ descendants << descendant
29
+ super
30
+ end
31
+
32
+ # Adapter name
33
+ #
34
+ # @example
35
+ # adapter.name # => :default
36
+ #
37
+ # Note that when you use
38
+ #
39
+ # DataMapper.setup(:default, 'postgres://postgres@localhost/dm_core_test')
40
+ #
41
+ # the adapter name is currently set to :default
42
+ #
43
+ # @return [Symbol]
44
+ # the adapter name
45
+ #
46
+ # @api semipublic
47
+ attr_reader :name
48
+
49
+ # Options with which adapter was set up
50
+ #
51
+ # @example
52
+ # adapter.options # => { :adapter => 'yaml', :path => '/tmp' }
53
+ #
54
+ # @return [Hash]
55
+ # adapter configuration options
56
+ #
57
+ # @api semipublic
58
+ attr_reader :options
59
+
60
+ # A callable object returning a naming convention for model storage
61
+ #
62
+ # @example
63
+ # adapter.resource_naming_convention # => Proc for model storage name
64
+ #
65
+ # @return [#call]
66
+ # object to return the naming convention for each model
67
+ #
68
+ # @api semipublic
69
+ attr_accessor :resource_naming_convention
70
+
71
+ # A callable object returning a naming convention for property fields
72
+ #
73
+ # @example
74
+ # adapter.field_naming_convention # => Proc for field name
75
+ #
76
+ # @return [#call]
77
+ # object to return the naming convention for each field
78
+ #
79
+ # @api semipublic
80
+ attr_accessor :field_naming_convention
81
+
82
+ # Persists one or many new resources
83
+ #
84
+ # @example
85
+ # adapter.create(collection) # => 1
86
+ #
87
+ # Adapters provide specific implementation of this method
88
+ #
89
+ # @param [Enumerable<Resource>] resources
90
+ # The list of resources (model instances) to create
91
+ #
92
+ # @return [Integer]
93
+ # The number of records that were actually saved into the data-store
94
+ #
95
+ # @api semipublic
96
+ def create(resources)
97
+ raise NotImplementedError, "#{self.class}#create not implemented"
98
+ end
99
+
100
+ # Reads one or many resources from a datastore
101
+ #
102
+ # @example
103
+ # adapter.read(query) # => [ { 'name' => 'Dan Kubb' } ]
104
+ #
105
+ # Adapters provide specific implementation of this method
106
+ #
107
+ # @param [Query] query
108
+ # the query to match resources in the datastore
109
+ #
110
+ # @return [Enumerable<Hash>]
111
+ # an array of hashes to become resources
112
+ #
113
+ # @api semipublic
114
+ def read(query)
115
+ raise NotImplementedError, "#{self.class}#read not implemented"
116
+ end
117
+
118
+ # Updates one or many existing resources
119
+ #
120
+ # @example
121
+ # adapter.update(attributes, collection) # => 1
122
+ #
123
+ # Adapters provide specific implementation of this method
124
+ #
125
+ # @param [Hash(Property => Object)] attributes
126
+ # hash of attribute values to set, keyed by Property
127
+ # @param [Collection] collection
128
+ # collection of records to be updated
129
+ #
130
+ # @return [Integer]
131
+ # the number of records updated
132
+ #
133
+ # @api semipublic
134
+ def update(attributes, collection)
135
+ raise NotImplementedError, "#{self.class}#update not implemented"
136
+ end
137
+
138
+ # Deletes one or many existing resources
139
+ #
140
+ # @example
141
+ # adapter.delete(collection) # => 1
142
+ #
143
+ # Adapters provide specific implementation of this method
144
+ #
145
+ # @param [Collection] collection
146
+ # collection of records to be deleted
147
+ #
148
+ # @return [Integer]
149
+ # the number of records deleted
150
+ #
151
+ # @api semipublic
152
+ def delete(collection)
153
+ raise NotImplementedError, "#{self.class}#delete not implemented"
154
+ end
155
+
156
+ # Create a Query object or subclass.
157
+ #
158
+ # Alter this method if you'd like to return an adapter specific Query subclass.
159
+ #
160
+ # @param [Repository] repository
161
+ # the Repository to retrieve results from
162
+ # @param [Model] model
163
+ # the Model to retrieve results from
164
+ # @param [Hash] options
165
+ # the conditions and scope
166
+ #
167
+ # @return [Query]
168
+ #
169
+ # @api semipublic
170
+ #--
171
+ # TODO: DataObjects::Connection.create_command style magic (Adapter)::Query?
172
+ def new_query(repository, model, options = {})
173
+ Query.new(repository, model, options)
174
+ end
175
+
176
+ protected
177
+
178
+ # Set the serial value of the Resource
179
+ #
180
+ # @param [Resource] resource
181
+ # the resource to set the serial property in
182
+ # @param [Integer] next_id
183
+ # the identifier to set in the resource
184
+ #
185
+ # @return [undefined]
186
+ #
187
+ # @api semipublic
188
+ def initialize_serial(resource, next_id)
189
+ return unless serial = resource.model.serial(name)
190
+ return unless serial.get!(resource).nil?
191
+ serial.set!(resource, next_id)
192
+
193
+ # TODO: replace above with this, once
194
+ # specs can handle random, non-sequential ids
195
+ #serial.set!(resource, rand(2**32))
196
+ end
197
+
198
+ # Translate the attributes into a Hash with the field as the key
199
+ #
200
+ # @example
201
+ # attributes = { User.properties[:name] => 'Dan Kubb' }
202
+ # adapter.attributes_as_fields(attributes) # => { 'name' => 'Dan Kubb' }
203
+ #
204
+ # @param [Hash] attributes
205
+ # the attributes with the Property as the key
206
+ #
207
+ # @return [Hash]
208
+ # the attributes with the Property#field as the key
209
+ #
210
+ # @api semipublic
211
+ def attributes_as_fields(attributes)
212
+ Hash[ attributes.map { |property, value| [ property.field, property.dump(value) ] } ]
213
+ end
214
+
215
+ private
216
+
217
+ # Initialize an AbstractAdapter instance
218
+ #
219
+ # @param [Symbol] name
220
+ # the adapter repository name
221
+ # @param [Hash] options
222
+ # the adapter configuration options
223
+ #
224
+ # @return [undefined]
225
+ #
226
+ # @api semipublic
227
+ def initialize(name, options)
228
+ @name = name
229
+ @options = options.dup.freeze
230
+ @resource_naming_convention = NamingConventions::Resource::UnderscoredAndPluralized
231
+ @field_naming_convention = NamingConventions::Field::Underscored
232
+ end
233
+ end # class AbstractAdapter
234
+
235
+ const_added(:AbstractAdapter)
236
+ end # module Adapters
237
+ end # module DataMapper
@@ -0,0 +1,113 @@
1
+ module DataMapper
2
+ module Adapters
3
+ # This is probably the simplest functional adapter possible. It simply
4
+ # stores and queries from a hash containing the model classes as keys,
5
+ # and an array of hashes. It is not persistent whatsoever; when the Ruby
6
+ # process finishes, everything that was stored it lost. However, it doesn't
7
+ # require any other external libraries, such as data_objects, so it is ideal
8
+ # for writing specs against. It also serves as an excellent example for
9
+ # budding adapter developers, so it is critical that it remains well documented
10
+ # and up to date.
11
+ class InMemoryAdapter < AbstractAdapter
12
+ # Used by DataMapper to put records into a data-store: "INSERT" in SQL-speak.
13
+ # It takes an array of the resources (model instances) to be saved. Resources
14
+ # each have a key that can be used to quickly look them up later without
15
+ # searching, if the adapter supports it.
16
+ #
17
+ # @param [Enumerable(Resource)] resources
18
+ # The set of resources (model instances)
19
+ #
20
+ # @api semipublic
21
+ def create(resources)
22
+ records = records_for(resources.first.model)
23
+
24
+ resources.each do |resource|
25
+ initialize_serial(resource, records.size.succ)
26
+ records << attributes_as_fields(resource.attributes(nil))
27
+ end
28
+ end
29
+
30
+ # Looks up one record or a collection of records from the data-store:
31
+ # "SELECT" in SQL.
32
+ #
33
+ # @param [Query] query
34
+ # The query to be used to seach for the resources
35
+ #
36
+ # @return [Array]
37
+ # An Array of Hashes containing the key-value pairs for
38
+ # each record
39
+ #
40
+ # @api semipublic
41
+ def read(query)
42
+ query.filter_records(records_for(query.model).dup)
43
+ end
44
+
45
+ # Used by DataMapper to update the attributes on existing records in a
46
+ # data-store: "UPDATE" in SQL-speak. It takes a hash of the attributes
47
+ # to update with, as well as a collection object that specifies which resources
48
+ # should be updated.
49
+ #
50
+ # @param [Hash] attributes
51
+ # A set of key-value pairs of the attributes to update the resources with.
52
+ # @param [DataMapper::Collection] resources
53
+ # The collection of resources to update.
54
+ #
55
+ # @api semipublic
56
+ def update(attributes, collection)
57
+ attributes = attributes_as_fields(attributes)
58
+ read(collection.query).each { |record| record.update(attributes) }.size
59
+ end
60
+
61
+ # Destroys all the records matching the given query. "DELETE" in SQL.
62
+ #
63
+ # @param [DataMapper::Collection] resources
64
+ # The collection of resources to delete.
65
+ #
66
+ # @return [Integer]
67
+ # The number of records that were deleted.
68
+ #
69
+ # @api semipublic
70
+ def delete(collection)
71
+ records = records_for(collection.model)
72
+ records_to_delete = collection.query.filter_records(records.dup)
73
+ records.replace(records - records_to_delete)
74
+ records_to_delete.size
75
+ end
76
+
77
+ # TODO consider proper automigrate functionality
78
+ def reset
79
+ @records = {}
80
+ end
81
+
82
+ private
83
+
84
+ # Make a new instance of the adapter. The @records ivar is the 'data-store'
85
+ # for this adapter. It is not shared amongst multiple incarnations of this
86
+ # adapter, eg DataMapper.setup(:default, :adapter => :in_memory);
87
+ # DataMapper.setup(:alternate, :adapter => :in_memory) do not share the
88
+ # data-store between them.
89
+ #
90
+ # @param [String, Symbol] name
91
+ # The name of the Repository using this adapter.
92
+ # @param [String, Hash] uri_or_options
93
+ # The connection uri string, or a hash of options to set up
94
+ # the adapter
95
+ #
96
+ # @api semipublic
97
+ def initialize(name, options = {})
98
+ super
99
+ @records = {}
100
+ end
101
+
102
+ # All the records we're storing. This method will look them up by model name
103
+ #
104
+ # @api private
105
+ def records_for(model)
106
+ @records[model.storage_name(name)] ||= []
107
+ end
108
+
109
+ end # class InMemoryAdapter
110
+
111
+ const_added(:InMemoryAdapter)
112
+ end # module Adapters
113
+ end # module DataMapper
@@ -0,0 +1,499 @@
1
+ module DataMapper
2
+ module Associations
3
+ module ManyToMany #:nodoc:
4
+ class Relationship < Associations::OneToMany::Relationship
5
+ extend Chainable
6
+
7
+ OPTIONS = superclass::OPTIONS.dup << :through << :via
8
+
9
+ # Returns a set of keys that identify the target model
10
+ #
11
+ # @return [DataMapper::PropertySet]
12
+ # a set of properties that identify the target model
13
+ #
14
+ # @api semipublic
15
+ def child_key
16
+ return @child_key if defined?(@child_key)
17
+
18
+ repository_name = child_repository_name || parent_repository_name
19
+ properties = child_model.properties(repository_name)
20
+
21
+ @child_key = if @child_properties
22
+ child_key = properties.values_at(*@child_properties)
23
+ properties.class.new(child_key).freeze
24
+ else
25
+ properties.key
26
+ end
27
+ end
28
+
29
+ # @api semipublic
30
+ alias_method :target_key, :child_key
31
+
32
+ # Intermediate association for through model
33
+ # relationships
34
+ #
35
+ # Example: for :bugs association in
36
+ #
37
+ # class Software::Engineer
38
+ # include DataMapper::Resource
39
+ #
40
+ # has n, :missing_tests
41
+ # has n, :bugs, :through => :missing_tests
42
+ # end
43
+ #
44
+ # through is :missing_tests
45
+ #
46
+ # TODO: document a case when
47
+ # through option is a model and
48
+ # not an association name
49
+ #
50
+ # @api semipublic
51
+ def through
52
+ return @through if defined?(@through)
53
+
54
+ @through = options[:through]
55
+
56
+ if @through.kind_of?(Associations::Relationship)
57
+ return @through
58
+ end
59
+
60
+ model = source_model
61
+ repository_name = source_repository_name
62
+ relationships = model.relationships(repository_name)
63
+ name = through_relationship_name
64
+
65
+ @through = relationships[name] ||
66
+ DataMapper.repository(repository_name) do
67
+ model.has(min..max, name, through_model, one_to_many_options)
68
+ end
69
+
70
+ @through.child_key
71
+
72
+ @through
73
+ end
74
+
75
+ # @api semipublic
76
+ def via
77
+ return @via if defined?(@via)
78
+
79
+ @via = options[:via]
80
+
81
+ if @via.kind_of?(Associations::Relationship)
82
+ return @via
83
+ end
84
+
85
+ name = self.name
86
+ through = self.through
87
+ repository_name = through.relative_target_repository_name
88
+ through_model = through.target_model
89
+ relationships = through_model.relationships(repository_name)
90
+ singular_name = DataMapper::Inflector.singularize(name.to_s).to_sym
91
+
92
+ @via = relationships[@via] ||
93
+ relationships[name] ||
94
+ relationships[singular_name]
95
+
96
+ @via ||= if anonymous_through_model?
97
+ DataMapper.repository(repository_name) do
98
+ through_model.belongs_to(singular_name, target_model, many_to_one_options)
99
+ end
100
+ else
101
+ raise UnknownRelationshipError, "No relationships named #{name} or #{singular_name} in #{through_model}"
102
+ end
103
+
104
+ @via.child_key
105
+
106
+ @via
107
+ end
108
+
109
+ # @api semipublic
110
+ def links
111
+ return @links if defined?(@links)
112
+
113
+ @links = []
114
+ links = [ through, via ]
115
+
116
+ while relationship = links.shift
117
+ if relationship.respond_to?(:links)
118
+ links.unshift(*relationship.links)
119
+ else
120
+ @links << relationship
121
+ end
122
+ end
123
+
124
+ @links.freeze
125
+ end
126
+
127
+ # Initialize the chain for "many to many" relationships
128
+ #
129
+ # @return [self]
130
+ #
131
+ # @api public
132
+ def finalize
133
+ through
134
+ via
135
+ self
136
+ end
137
+
138
+ # @api private
139
+ def source_scope(source)
140
+ { through.inverse => source }
141
+ end
142
+
143
+ # @api private
144
+ def query
145
+ # TODO: consider making this a query_for method, so that ManyToMany::Relationship#query only
146
+ # returns the query supplied in the definition
147
+ @many_to_many_query ||= super.merge(:links => links).freeze
148
+ end
149
+
150
+ # Eager load the collection using the source as a base
151
+ #
152
+ # @param [Resource, Collection] source
153
+ # the source to query with
154
+ # @param [Query, Hash] other_query
155
+ # optional query to restrict the collection
156
+ #
157
+ # @return [ManyToMany::Collection]
158
+ # the loaded collection for the source
159
+ #
160
+ # @api private
161
+ def eager_load(source, other_query = nil)
162
+ # FIXME: enable SEL for m:m relationships
163
+ source.model.all(query_for(source, other_query))
164
+ end
165
+
166
+ private
167
+
168
+ # @api private
169
+ def through_model
170
+ namespace, name = through_model_namespace_name
171
+
172
+ if namespace.const_defined?(name)
173
+ namespace.const_get(name)
174
+ else
175
+ Model.new(name, namespace) do
176
+ # all properties added to the anonymous through model are keys
177
+ def property(name, type, options = {})
178
+ options[:key] = true
179
+ options.delete(:index)
180
+ super
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ # @api private
187
+ def through_model_namespace_name
188
+ target_parts = target_model.base_model.name.split('::')
189
+ source_parts = source_model.base_model.name.split('::')
190
+
191
+ name = [ target_parts.pop, source_parts.pop ].sort.join
192
+
193
+ namespace = Object
194
+
195
+ # find the common namespace between the target_model and source_model
196
+ target_parts.zip(source_parts) do |target_part, source_part|
197
+ break if target_part != source_part
198
+ namespace = namespace.const_get(target_part)
199
+ end
200
+
201
+ return namespace, name
202
+ end
203
+
204
+ # @api private
205
+ def through_relationship_name
206
+ if anonymous_through_model?
207
+ namespace = through_model_namespace_name.first
208
+ relationship_name = DataMapper::Inflector.underscore(through_model.name.sub(/\A#{namespace.name}::/, '')).tr('/', '_')
209
+ DataMapper::Inflector.pluralize(relationship_name).to_sym
210
+ else
211
+ options[:through]
212
+ end
213
+ end
214
+
215
+ # Check if the :through association uses an anonymous model
216
+ #
217
+ # An anonymous model means that DataMapper creates the model
218
+ # in-memory, and sets the relationships to join the source
219
+ # and the target model.
220
+ #
221
+ # @return [Boolean]
222
+ # true if the through model is anonymous
223
+ #
224
+ # @api private
225
+ def anonymous_through_model?
226
+ options[:through] == Resource
227
+ end
228
+
229
+ # @api private
230
+ def nearest_relationship
231
+ return @nearest_relationship if defined?(@nearest_relationship)
232
+
233
+ nearest_relationship = self
234
+
235
+ while nearest_relationship.respond_to?(:through)
236
+ nearest_relationship = nearest_relationship.through
237
+ end
238
+
239
+ @nearest_relationship = nearest_relationship
240
+ end
241
+
242
+ # @api private
243
+ def valid_target?(target)
244
+ relationship = via
245
+ source_key = relationship.source_key
246
+ target_key = relationship.target_key
247
+
248
+ target.kind_of?(target_model) &&
249
+ source_key.valid?(target_key.get(target))
250
+ end
251
+
252
+ # @api private
253
+ def valid_source?(source)
254
+ relationship = nearest_relationship
255
+ source_key = relationship.source_key
256
+ target_key = relationship.target_key
257
+
258
+ source.kind_of?(source_model) &&
259
+ target_key.valid?(source_key.get(source))
260
+ end
261
+
262
+ chainable do
263
+ # @api semipublic
264
+ def many_to_one_options
265
+ { :parent_key => target_key.map { |property| property.name } }
266
+ end
267
+
268
+ # @api semipublic
269
+ def one_to_many_options
270
+ { :parent_key => source_key.map { |property| property.name } }
271
+ end
272
+ end
273
+
274
+ # Returns the inverse relationship class
275
+ #
276
+ # @api private
277
+ def inverse_class
278
+ self.class
279
+ end
280
+
281
+ # @api private
282
+ def invert
283
+ inverse_class.new(inverse_name, parent_model, child_model, inverted_options)
284
+ end
285
+
286
+ # @api private
287
+ def inverted_options
288
+ links = self.links.dup
289
+ through = links.pop.inverse
290
+
291
+ links.reverse_each do |relationship|
292
+ inverse = relationship.inverse
293
+
294
+ through = self.class.new(
295
+ inverse.name,
296
+ inverse.child_model,
297
+ inverse.parent_model,
298
+ inverse.options.merge(:through => through)
299
+ )
300
+ end
301
+
302
+ options = self.options
303
+
304
+ DataMapper::Ext::Hash.only(options, *OPTIONS - [ :min, :max ]).update(
305
+ :through => through,
306
+ :child_key => options[:parent_key],
307
+ :parent_key => options[:child_key],
308
+ :inverse => self
309
+ )
310
+ end
311
+
312
+ # Returns collection class used by this type of
313
+ # relationship
314
+ #
315
+ # @api private
316
+ def collection_class
317
+ ManyToMany::Collection
318
+ end
319
+ end # class Relationship
320
+
321
+ class Collection < Associations::OneToMany::Collection
322
+ # Remove every Resource in the m:m Collection from the repository
323
+ #
324
+ # This performs a deletion of each Resource in the Collection from
325
+ # the repository and clears the Collection.
326
+ #
327
+ # @return [Boolean]
328
+ # true if the resources were successfully destroyed
329
+ #
330
+ # @api public
331
+ def destroy
332
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
333
+
334
+ # make sure the records are loaded so they can be found when
335
+ # the intermediaries are removed
336
+ lazy_load
337
+
338
+ unless intermediaries.all(via => self).destroy
339
+ return false
340
+ end
341
+
342
+ super
343
+ end
344
+
345
+ # Remove every Resource in the m:m Collection from the repository, bypassing validation
346
+ #
347
+ # This performs a deletion of each Resource in the Collection from
348
+ # the repository and clears the Collection while skipping
349
+ # validation.
350
+ #
351
+ # @return [Boolean]
352
+ # true if the resources were successfully destroyed
353
+ #
354
+ # @api public
355
+ def destroy!
356
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
357
+
358
+ model = self.model
359
+ key = model.key(repository_name)
360
+ conditions = Query.target_conditions(self, key, key)
361
+
362
+ unless intermediaries.all(via => self).destroy!
363
+ return false
364
+ end
365
+
366
+ unless model.all(:repository => repository, :conditions => conditions).destroy!
367
+ return false
368
+ end
369
+
370
+ each do |resource|
371
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
372
+ end
373
+
374
+ clear
375
+
376
+ true
377
+ end
378
+
379
+ # Return the intermediaries linking the source to the targets
380
+ #
381
+ # @return [Collection]
382
+ # the intermediary collection
383
+ #
384
+ # @api public
385
+ def intermediaries
386
+ through = self.through
387
+ source = self.source
388
+
389
+ @intermediaries ||= if through.loaded?(source)
390
+ through.get_collection(source)
391
+ else
392
+ reset_intermediaries
393
+ end
394
+ end
395
+
396
+ protected
397
+
398
+ # Map the resources in the collection to the intermediaries
399
+ #
400
+ # @return [Hash]
401
+ # the map of resources to their intermediaries
402
+ #
403
+ # @api private
404
+ def intermediary_for
405
+ @intermediary_for ||= {}
406
+ end
407
+
408
+ # @api private
409
+ def through
410
+ relationship.through
411
+ end
412
+
413
+ # @api private
414
+ def via
415
+ relationship.via
416
+ end
417
+
418
+ private
419
+
420
+ # @api private
421
+ def _create(attributes, execute_hooks = true)
422
+ via = self.via
423
+ if via.respond_to?(:resource_for)
424
+ resource = super
425
+ if create_intermediary(execute_hooks, resource)
426
+ resource
427
+ end
428
+ else
429
+ if intermediary = create_intermediary(execute_hooks)
430
+ super(attributes.merge(via.inverse => intermediary), execute_hooks)
431
+ end
432
+ end
433
+ end
434
+
435
+ # @api private
436
+ def _save(execute_hooks = true)
437
+ via = self.via
438
+
439
+ if @removed.any?
440
+ # delete only intermediaries linked to the removed targets
441
+ return false unless intermediaries.all(via => @removed).send(execute_hooks ? :destroy : :destroy!)
442
+
443
+ # reset the intermediaries so that it reflects the current state of the datastore
444
+ reset_intermediaries
445
+ end
446
+
447
+ loaded_entries = self.loaded_entries
448
+
449
+ if via.respond_to?(:resource_for)
450
+ super
451
+ loaded_entries.all? { |resource| create_intermediary(execute_hooks, resource) }
452
+ else
453
+ if loaded_entries.any? && (intermediary = create_intermediary(execute_hooks))
454
+ inverse = via.inverse
455
+ loaded_entries.each { |resource| inverse.set(resource, intermediary) }
456
+ end
457
+
458
+ super
459
+ end
460
+ end
461
+
462
+ # @api private
463
+ def create_intermediary(execute_hooks, resource = nil)
464
+ intermediary_for = self.intermediary_for
465
+
466
+ intermediary_resource = intermediary_for[resource]
467
+ return intermediary_resource if intermediary_resource
468
+
469
+ intermediaries = self.intermediaries
470
+ method = execute_hooks ? :save : :save!
471
+
472
+ return unless intermediaries.send(method)
473
+
474
+ attributes = {}
475
+ attributes[via] = resource if resource
476
+
477
+ intermediary = intermediaries.first_or_new(attributes)
478
+ return unless intermediary.__send__(method)
479
+
480
+ # map the resource, even if it is nil, to the intermediary
481
+ intermediary_for[resource] = intermediary
482
+ end
483
+
484
+ # @api private
485
+ def reset_intermediaries
486
+ through = self.through
487
+ source = self.source
488
+
489
+ through.set_collection(source, through.collection_for(source))
490
+ end
491
+
492
+ # @api private
493
+ def inverse_set(*)
494
+ # do nothing
495
+ end
496
+ end # class Collection
497
+ end # module ManyToMany
498
+ end # module Associations
499
+ end # module DataMapper