ardm-core 1.2.1

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