ghost_dm-core 1.3.0.beta

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 (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