ardm-core 1.2.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 (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