sbf-dm-core 1.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (259) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +29 -0
  3. data/.document +5 -0
  4. data/.gitignore +44 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +468 -0
  7. data/.travis.yml +57 -0
  8. data/.yardopts +1 -0
  9. data/Gemfile +70 -0
  10. data/LICENSE +20 -0
  11. data/README.md +269 -0
  12. data/Rakefile +4 -0
  13. data/dm-core.gemspec +21 -0
  14. data/lib/dm-core/adapters/abstract_adapter.rb +233 -0
  15. data/lib/dm-core/adapters/in_memory_adapter.rb +110 -0
  16. data/lib/dm-core/adapters.rb +249 -0
  17. data/lib/dm-core/associations/many_to_many.rb +477 -0
  18. data/lib/dm-core/associations/many_to_one.rb +282 -0
  19. data/lib/dm-core/associations/one_to_many.rb +332 -0
  20. data/lib/dm-core/associations/one_to_one.rb +84 -0
  21. data/lib/dm-core/associations/relationship.rb +650 -0
  22. data/lib/dm-core/backwards.rb +11 -0
  23. data/lib/dm-core/collection.rb +1486 -0
  24. data/lib/dm-core/core_ext/kernel.rb +21 -0
  25. data/lib/dm-core/core_ext/pathname.rb +4 -0
  26. data/lib/dm-core/core_ext/symbol.rb +10 -0
  27. data/lib/dm-core/identity_map.rb +6 -0
  28. data/lib/dm-core/model/hook.rb +99 -0
  29. data/lib/dm-core/model/is.rb +30 -0
  30. data/lib/dm-core/model/property.rb +244 -0
  31. data/lib/dm-core/model/relationship.rb +366 -0
  32. data/lib/dm-core/model/scope.rb +87 -0
  33. data/lib/dm-core/model.rb +876 -0
  34. data/lib/dm-core/property/binary.rb +19 -0
  35. data/lib/dm-core/property/boolean.rb +35 -0
  36. data/lib/dm-core/property/class.rb +23 -0
  37. data/lib/dm-core/property/date.rb +45 -0
  38. data/lib/dm-core/property/date_time.rb +44 -0
  39. data/lib/dm-core/property/decimal.rb +47 -0
  40. data/lib/dm-core/property/discriminator.rb +40 -0
  41. data/lib/dm-core/property/float.rb +27 -0
  42. data/lib/dm-core/property/integer.rb +32 -0
  43. data/lib/dm-core/property/invalid_value_error.rb +17 -0
  44. data/lib/dm-core/property/lookup.rb +26 -0
  45. data/lib/dm-core/property/numeric.rb +35 -0
  46. data/lib/dm-core/property/object.rb +33 -0
  47. data/lib/dm-core/property/serial.rb +13 -0
  48. data/lib/dm-core/property/string.rb +47 -0
  49. data/lib/dm-core/property/text.rb +12 -0
  50. data/lib/dm-core/property/time.rb +46 -0
  51. data/lib/dm-core/property/typecast/numeric.rb +32 -0
  52. data/lib/dm-core/property/typecast/time.rb +33 -0
  53. data/lib/dm-core/property.rb +856 -0
  54. data/lib/dm-core/property_set.rb +177 -0
  55. data/lib/dm-core/query/conditions/comparison.rb +886 -0
  56. data/lib/dm-core/query/conditions/operation.rb +710 -0
  57. data/lib/dm-core/query/direction.rb +33 -0
  58. data/lib/dm-core/query/operator.rb +34 -0
  59. data/lib/dm-core/query/path.rb +113 -0
  60. data/lib/dm-core/query/sort.rb +38 -0
  61. data/lib/dm-core/query.rb +1352 -0
  62. data/lib/dm-core/relationship_set.rb +69 -0
  63. data/lib/dm-core/repository.rb +226 -0
  64. data/lib/dm-core/resource/persistence_state/clean.rb +36 -0
  65. data/lib/dm-core/resource/persistence_state/deleted.rb +26 -0
  66. data/lib/dm-core/resource/persistence_state/dirty.rb +91 -0
  67. data/lib/dm-core/resource/persistence_state/immutable.rb +32 -0
  68. data/lib/dm-core/resource/persistence_state/persisted.rb +25 -0
  69. data/lib/dm-core/resource/persistence_state/transient.rb +87 -0
  70. data/lib/dm-core/resource/persistence_state.rb +70 -0
  71. data/lib/dm-core/resource.rb +1220 -0
  72. data/lib/dm-core/spec/lib/adapter_helpers.rb +63 -0
  73. data/lib/dm-core/spec/lib/collection_helpers.rb +21 -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 +164 -0
  78. data/lib/dm-core/spec/shared/adapter_spec.rb +366 -0
  79. data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
  80. data/lib/dm-core/spec/shared/resource_spec.rb +1221 -0
  81. data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
  82. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +184 -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 +388 -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 +13 -0
  102. data/lib/dm-core/support/logger.rb +201 -0
  103. data/lib/dm-core/support/mash.rb +176 -0
  104. data/lib/dm-core/support/naming_conventions.rb +109 -0
  105. data/lib/dm-core/support/ordered_set.rb +381 -0
  106. data/lib/dm-core/support/subject.rb +33 -0
  107. data/lib/dm-core/support/subject_set.rb +251 -0
  108. data/lib/dm-core/version.rb +3 -0
  109. data/lib/dm-core.rb +274 -0
  110. data/script/performance.rb +275 -0
  111. data/script/profile.rb +218 -0
  112. data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
  113. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +69 -0
  114. data/spec/public/associations/many_to_many_spec.rb +197 -0
  115. data/spec/public/associations/many_to_one_spec.rb +83 -0
  116. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
  117. data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
  118. data/spec/public/associations/one_to_many_spec.rb +81 -0
  119. data/spec/public/associations/one_to_one_spec.rb +176 -0
  120. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
  121. data/spec/public/collection_spec.rb +69 -0
  122. data/spec/public/finalize_spec.rb +77 -0
  123. data/spec/public/model/hook_spec.rb +245 -0
  124. data/spec/public/model/property_spec.rb +91 -0
  125. data/spec/public/model/relationship_spec.rb +1040 -0
  126. data/spec/public/model_spec.rb +456 -0
  127. data/spec/public/property/binary_spec.rb +43 -0
  128. data/spec/public/property/boolean_spec.rb +21 -0
  129. data/spec/public/property/class_spec.rb +27 -0
  130. data/spec/public/property/date_spec.rb +21 -0
  131. data/spec/public/property/date_time_spec.rb +21 -0
  132. data/spec/public/property/decimal_spec.rb +23 -0
  133. data/spec/public/property/discriminator_spec.rb +134 -0
  134. data/spec/public/property/float_spec.rb +22 -0
  135. data/spec/public/property/integer_spec.rb +22 -0
  136. data/spec/public/property/object_spec.rb +117 -0
  137. data/spec/public/property/serial_spec.rb +22 -0
  138. data/spec/public/property/string_spec.rb +21 -0
  139. data/spec/public/property/text_spec.rb +62 -0
  140. data/spec/public/property/time_spec.rb +21 -0
  141. data/spec/public/property_spec.rb +333 -0
  142. data/spec/public/resource/state_spec.rb +72 -0
  143. data/spec/public/resource_spec.rb +289 -0
  144. data/spec/public/sel_spec.rb +53 -0
  145. data/spec/public/setup_spec.rb +145 -0
  146. data/spec/public/shared/association_collection_shared_spec.rb +309 -0
  147. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  148. data/spec/public/shared/collection_shared_spec.rb +1637 -0
  149. data/spec/public/shared/finder_shared_spec.rb +1647 -0
  150. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  151. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
  152. data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
  153. data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
  154. data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
  155. data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
  156. data/spec/semipublic/associations/relationship_spec.rb +200 -0
  157. data/spec/semipublic/associations_spec.rb +177 -0
  158. data/spec/semipublic/collection_spec.rb +110 -0
  159. data/spec/semipublic/model_spec.rb +96 -0
  160. data/spec/semipublic/property/binary_spec.rb +13 -0
  161. data/spec/semipublic/property/boolean_spec.rb +47 -0
  162. data/spec/semipublic/property/class_spec.rb +33 -0
  163. data/spec/semipublic/property/date_spec.rb +43 -0
  164. data/spec/semipublic/property/date_time_spec.rb +46 -0
  165. data/spec/semipublic/property/decimal_spec.rb +83 -0
  166. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  167. data/spec/semipublic/property/float_spec.rb +82 -0
  168. data/spec/semipublic/property/integer_spec.rb +82 -0
  169. data/spec/semipublic/property/lookup_spec.rb +29 -0
  170. data/spec/semipublic/property/serial_spec.rb +13 -0
  171. data/spec/semipublic/property/string_spec.rb +13 -0
  172. data/spec/semipublic/property/text_spec.rb +31 -0
  173. data/spec/semipublic/property/time_spec.rb +50 -0
  174. data/spec/semipublic/property_spec.rb +114 -0
  175. data/spec/semipublic/query/conditions/comparison_spec.rb +1502 -0
  176. data/spec/semipublic/query/conditions/operation_spec.rb +1296 -0
  177. data/spec/semipublic/query/path_spec.rb +471 -0
  178. data/spec/semipublic/query_spec.rb +3665 -0
  179. data/spec/semipublic/resource/state/clean_spec.rb +89 -0
  180. data/spec/semipublic/resource/state/deleted_spec.rb +79 -0
  181. data/spec/semipublic/resource/state/dirty_spec.rb +163 -0
  182. data/spec/semipublic/resource/state/immutable_spec.rb +107 -0
  183. data/spec/semipublic/resource/state/transient_spec.rb +163 -0
  184. data/spec/semipublic/resource/state_spec.rb +230 -0
  185. data/spec/semipublic/resource_spec.rb +23 -0
  186. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  187. data/spec/semipublic/shared/resource_shared_spec.rb +198 -0
  188. data/spec/semipublic/shared/resource_state_shared_spec.rb +91 -0
  189. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  190. data/spec/spec_helper.rb +34 -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 +27 -0
  248. data/spec/unit/hook_spec.rb +1216 -0
  249. data/spec/unit/inflections_spec.rb +14 -0
  250. data/spec/unit/lazy_array_spec.rb +1949 -0
  251. data/spec/unit/mash_spec.rb +289 -0
  252. data/spec/unit/module_spec.rb +70 -0
  253. data/spec/unit/object_spec.rb +38 -0
  254. data/spec/unit/try_dup_spec.rb +46 -0
  255. data/tasks/ci.rake +1 -0
  256. data/tasks/spec.rake +18 -0
  257. data/tasks/yard.rake +9 -0
  258. data/tasks/yardstick.rake +19 -0
  259. metadata +323 -0
@@ -0,0 +1,1486 @@
1
+ # TODO: if Collection is scoped by a unique property, should adding
2
+ # new Resources be denied?
3
+
4
+ # TODO: add #copy method
5
+
6
+ # TODO: move Collection#loaded_entries to LazyArray
7
+ # TODO: move Collection#partially_loaded to LazyArray
8
+
9
+ module DataMapper
10
+ # The Collection class represents a list of resources persisted in
11
+ # a repository and identified by a query.
12
+ #
13
+ # A Collection should act like an Array in every way, except that
14
+ # it will attempt to defer loading until the results from the
15
+ # repository are needed.
16
+ #
17
+ # A Collection is typically returned by the Model#all
18
+ # method.
19
+ class Collection < LazyArray
20
+ # Returns the Query the Collection is scoped with
21
+ #
22
+ # @return [Query]
23
+ # the Query the Collection is scoped with
24
+ #
25
+ # @api semipublic
26
+ attr_reader :query
27
+
28
+ # Returns the Repository
29
+ #
30
+ # @return [Repository]
31
+ # the Repository this Collection is associated with
32
+ #
33
+ # @api semipublic
34
+ def repository
35
+ query.repository
36
+ end
37
+
38
+ # Returns the Model
39
+ #
40
+ # @return [Model]
41
+ # the Model the Collection is associated with
42
+ #
43
+ # @api semipublic
44
+ def model
45
+ query.model
46
+ end
47
+
48
+ # Reloads the Collection from the repository
49
+ #
50
+ # If +query+ is provided, updates this Collection's query with its conditions
51
+ #
52
+ # cars_from_91 = Cars.all(:year_manufactured => 1991)
53
+ # cars_from_91.first.year_manufactured = 2001 # note: not saved
54
+ # cars_from_91.reload
55
+ # cars_from_91.first.year #=> 1991
56
+ #
57
+ # @param [Query, Hash] other_query (optional)
58
+ # further restrict results with query
59
+ #
60
+ # @return [self]
61
+ #
62
+ # @api public
63
+ def reload(other_query = Undefined)
64
+ query = self.query
65
+ query = other_query.equal?(Undefined) ? query.dup : query.merge(other_query)
66
+
67
+ # make sure the Identity Map contains all the existing resources
68
+ identity_map = repository.identity_map(model)
69
+
70
+ loaded_entries.each do |resource|
71
+ identity_map[resource.key] = resource
72
+ end
73
+
74
+ # sort fields based on declared order, for more consistent reload queries
75
+ properties = self.properties
76
+ fields = properties & (query.fields | model_key | [properties.discriminator].compact)
77
+
78
+ # replace the list of resources
79
+ replace(all(query.update(fields: fields, reload: true)))
80
+ end
81
+
82
+ # Return the union with another collection
83
+ #
84
+ # @param [Collection] other
85
+ # the other collection
86
+ #
87
+ # @return [Collection]
88
+ # the union of the collection and other
89
+ #
90
+ # @api public
91
+ def union(other)
92
+ set_operation(:|, other)
93
+ end
94
+
95
+ alias_method :|, :union
96
+ alias_method :+, :union
97
+
98
+ # Return the intersection with another collection
99
+ #
100
+ # @param [Collection] other
101
+ # the other collection
102
+ #
103
+ # @return [Collection]
104
+ # the intersection of the collection and other
105
+ #
106
+ # @api public
107
+ def intersection(other)
108
+ set_operation(:&, other)
109
+ end
110
+
111
+ alias_method :&, :intersection
112
+
113
+ # Return the difference with another collection
114
+ #
115
+ # @param [Collection] other
116
+ # the other collection
117
+ #
118
+ # @return [Collection]
119
+ # the difference of the collection and other
120
+ #
121
+ # @api public
122
+ def difference(other)
123
+ set_operation(:-, other)
124
+ end
125
+
126
+ alias_method :-, :difference
127
+
128
+ # Lookup a Resource in the Collection by key
129
+ #
130
+ # This looks up a Resource by key, typecasting the key to the
131
+ # proper object if necessary.
132
+ #
133
+ # toyotas = Cars.all(:manufacturer => 'Toyota')
134
+ # toyo = Cars.first(:manufacturer => 'Toyota')
135
+ # toyotas.get(toyo.id) == toyo #=> true
136
+ #
137
+ # @param [Enumerable] *key
138
+ # keys which uniquely identify a resource in the Collection
139
+ #
140
+ # @return [Resource]
141
+ # Resource which matches the supplied key
142
+ # @return [nil]
143
+ # No Resource matches the supplied key
144
+ #
145
+ # @api public
146
+ def get(*key)
147
+ assert_valid_key_size(key)
148
+
149
+ key = model_key.typecast(key)
150
+ query = self.query
151
+
152
+ @identity_map[key] || if !loaded? && (query.limit || query.offset > 0)
153
+ # current query is exclusive, find resource within the set
154
+
155
+ # TODO: use a subquery to retrieve the Collection and then match
156
+ # it up against the key. This will require some changes to
157
+ # how subqueries are generated, since the key may be a
158
+ # composite key. In the case of DO adapters, it means subselects
159
+ # like the form "(a, b) IN(SELECT a, b FROM ...)", which will
160
+ # require making it so the Query condition key can be a
161
+ # Property or an Array of Property objects
162
+
163
+ # use the brute force approach until subquery lookups work
164
+ lazy_load
165
+ @identity_map[key]
166
+ else
167
+ # current query is all inclusive, lookup using normal approach
168
+ first(model.key_conditions(repository, key).update(order: nil))
169
+ end
170
+ end
171
+
172
+ # Lookup a Resource in the Collection by key, raising an exception if not found
173
+ #
174
+ # This looks up a Resource by key, typecasting the key to the
175
+ # proper object if necessary.
176
+ #
177
+ # @param [Enumerable] *key
178
+ # keys which uniquely identify a resource in the Collection
179
+ #
180
+ # @return [Resource]
181
+ # Resource which matches the supplied key
182
+ # @return [nil]
183
+ # No Resource matches the supplied key
184
+ #
185
+ # @raise [ObjectNotFoundError] Resource could not be found by key
186
+ #
187
+ # @api public
188
+ def get!(*key)
189
+ get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect}")
190
+ end
191
+
192
+ # Returns a new Collection optionally scoped by +query+
193
+ #
194
+ # This returns a new Collection scoped relative to the current
195
+ # Collection.
196
+ #
197
+ # cars_from_91 = Cars.all(:year_manufactured => 1991)
198
+ # toyotas_91 = cars_from_91.all(:manufacturer => 'Toyota')
199
+ # toyotas_91.all? { |car| car.year_manufactured == 1991 } #=> true
200
+ # toyotas_91.all? { |car| car.manufacturer == 'Toyota' } #=> true
201
+ #
202
+ # If +query+ is a Hash, results will be found by merging +query+ with this Collection's query.
203
+ # If +query+ is a Query, results will be found using +query+ as an absolute query.
204
+ #
205
+ # @param [Hash, Query] query
206
+ # optional parameters to scope results with
207
+ #
208
+ # @return [Collection]
209
+ # Collection scoped by +query+
210
+ #
211
+ # @api public
212
+ def all(query = Undefined)
213
+ if query.equal?(Undefined) || (query.is_a?(Hash) && query.empty?)
214
+ dup
215
+ else
216
+ # TODO: if there is no order parameter, and the Collection is not loaded
217
+ # check to see if the query can be satisfied by the head/tail
218
+ new_collection(scoped_query(query))
219
+ end
220
+ end
221
+
222
+ # Return the first Resource or the first N Resources in the Collection with an optional query
223
+ #
224
+ # When there are no arguments, return the first Resource in the
225
+ # Collection. When the first argument is an Integer, return a
226
+ # Collection containing the first N Resources. When the last
227
+ # (optional) argument is a Hash scope the results to the query.
228
+ #
229
+ # @param [Integer] limit (optional)
230
+ # limit the returned Collection to a specific number of entries
231
+ # @param [Hash] query (optional)
232
+ # scope the returned Resource or Collection to the supplied query
233
+ #
234
+ # @return [Resource, Collection]
235
+ # The first resource in the entries of this collection,
236
+ # or a new collection whose query has been merged
237
+ #
238
+ # @api public
239
+ def first(*args)
240
+ first_arg = args.first
241
+ last_arg = args.last
242
+
243
+ limit_specified = first_arg.is_a?(Integer)
244
+ with_query = (last_arg.is_a?(Hash) && !last_arg.empty?) || last_arg.is_a?(Query)
245
+
246
+ limit = limit_specified ? first_arg : 1
247
+ query = with_query ? last_arg : {}
248
+
249
+ query = self.query.slice(0, limit).update(query)
250
+
251
+ # TODO: when a query provided, and there are enough elements in head to
252
+ # satisfy the query.limit, filter the head with the query, and make
253
+ # sure it matches the limit exactly. if so, use that result instead
254
+ # of calling all()
255
+ # - this can probably only be done if there is no :order parameter
256
+
257
+ loaded = loaded?
258
+ head = self.head
259
+
260
+ collection = if !with_query && (loaded || lazy_possible?(head, limit))
261
+ new_collection(query, super(limit))
262
+ else
263
+ all(query)
264
+ end
265
+
266
+ return collection if limit_specified
267
+
268
+ resource = collection.to_a.first
269
+
270
+ if with_query || loaded
271
+ resource
272
+ elsif resource
273
+ head[0] = resource
274
+ end
275
+ end
276
+
277
+ # Return the last Resource or the last N Resources in the Collection with an optional query
278
+ #
279
+ # When there are no arguments, return the last Resource in the
280
+ # Collection. When the first argument is an Integer, return a
281
+ # Collection containing the last N Resources. When the last
282
+ # (optional) argument is a Hash scope the results to the query.
283
+ #
284
+ # @param [Integer] limit (optional)
285
+ # limit the returned Collection to a specific number of entries
286
+ # @param [Hash] query (optional)
287
+ # scope the returned Resource or Collection to the supplied query
288
+ #
289
+ # @return [Resource, Collection]
290
+ # The last resource in the entries of this collection,
291
+ # or a new collection whose query has been merged
292
+ #
293
+ # @api public
294
+ def last(*args)
295
+ first_arg = args.first
296
+ last_arg = args.last
297
+
298
+ limit_specified = first_arg.is_a?(Integer)
299
+ with_query = (last_arg.is_a?(Hash) && !last_arg.empty?) || last_arg.is_a?(Query)
300
+
301
+ limit = limit_specified ? first_arg : 1
302
+ query = with_query ? last_arg : {}
303
+
304
+ query = self.query.slice(0, limit).update(query).reverse!
305
+
306
+ # tell the Query to prepend each result from the adapter
307
+ query.update(add_reversed: !query.add_reversed?)
308
+
309
+ # TODO: when a query provided, and there are enough elements in tail to
310
+ # satisfy the query.limit, filter the tail with the query, and make
311
+ # sure it matches the limit exactly. if so, use that result instead
312
+ # of calling all()
313
+
314
+ loaded = loaded?
315
+ tail = self.tail
316
+
317
+ collection = if !with_query && (loaded || lazy_possible?(tail, limit))
318
+ new_collection(query, super(limit))
319
+ else
320
+ all(query)
321
+ end
322
+
323
+ return collection if limit_specified
324
+
325
+ resource = collection.to_a.last
326
+
327
+ if with_query || loaded
328
+ resource
329
+ elsif resource
330
+ tail[tail.empty? ? 0 : -1] = resource
331
+ end
332
+ end
333
+
334
+ # Lookup a Resource from the Collection by offset
335
+ #
336
+ # @param [Integer] offset
337
+ # offset of the Resource in the Collection
338
+ #
339
+ # @return [Resource]
340
+ # Resource which matches the supplied offset
341
+ # @return [nil]
342
+ # No Resource matches the supplied offset
343
+ #
344
+ # @api public
345
+ def at(offset)
346
+ if loaded? || partially_loaded?(offset)
347
+ super
348
+ elsif offset == 0
349
+ first
350
+ elsif offset > 0
351
+ first(offset: offset)
352
+ elsif offset == -1
353
+ last
354
+ else
355
+ last(offset: offset.abs - 1)
356
+ end
357
+ end
358
+
359
+ # Access LazyArray#slice directly
360
+ #
361
+ # Collection#[]= uses this to bypass Collection#slice and access
362
+ # the resources directly so that it can orphan them properly.
363
+ #
364
+ # @api private
365
+ alias_method :superclass_slice, :slice
366
+ private :superclass_slice
367
+
368
+ # Simulates Array#slice and returns a new Collection
369
+ # whose query has a new offset or limit according to the
370
+ # arguments provided.
371
+ #
372
+ # If you provide a range, the min is used as the offset
373
+ # and the max minus the offset is used as the limit.
374
+ #
375
+ # @param [Integer, Array(Integer), Range] *args
376
+ # the offset, offset and limit, or range indicating first and last position
377
+ #
378
+ # @return [Resource, Collection, nil]
379
+ # The entry which resides at that offset and limit,
380
+ # or a new Collection object with the set limits and offset
381
+ # @return [nil]
382
+ # The offset (or starting offset) is out of range
383
+ #
384
+ # @raise [ArgumentError] "arguments may be 1 or 2 Integers,
385
+ # or 1 Range object, was: #{args.inspect}"
386
+ #
387
+ # @api public
388
+ def [](*args)
389
+ offset, limit = extract_slice_arguments(*args)
390
+
391
+ return at(offset) if args.size == 1 && args.first.is_a?(Integer)
392
+
393
+ query = sliced_query(offset, limit)
394
+
395
+ if loaded? || partially_loaded?(offset, limit)
396
+ new_collection(query, super)
397
+ else
398
+ new_collection(query)
399
+ end
400
+ end
401
+
402
+ alias_method :slice, :[]
403
+
404
+ # Deletes and Returns the Resources given by an offset or a Range
405
+ #
406
+ # @param [Integer, Array(Integer), Range] *args
407
+ # the offset, offset and limit, or range indicating first and last position
408
+ #
409
+ # @return [Resource, Collection]
410
+ # The entry which resides at that offset and limit, or
411
+ # a new Collection object with the set limits and offset
412
+ # @return [Resource, Collection, nil]
413
+ # The offset is out of range
414
+ #
415
+ # @api public
416
+ def slice!(*args)
417
+ removed = super
418
+
419
+ resources_removed(removed) unless removed.nil?
420
+
421
+ # Workaround for Ruby <= 1.8.6
422
+ compact! if RUBY_VERSION <= '1.8.6'
423
+
424
+ return removed unless removed.is_a?(Enumerable)
425
+
426
+ offset, limit = extract_slice_arguments(*args)
427
+
428
+ query = sliced_query(offset, limit)
429
+
430
+ new_collection(query, removed)
431
+ end
432
+
433
+ # Splice a list of Resources at a given offset or range
434
+ #
435
+ # When nil is provided instead of a Resource or a list of Resources
436
+ # this will remove all of the Resources at the specified position.
437
+ #
438
+ # @param [Integer, Array(Integer), Range] *args
439
+ # The offset, offset and limit, or range indicating first and last position.
440
+ # The last argument may be a Resource, a list of Resources or nil.
441
+ #
442
+ # @return [Resource, Enumerable]
443
+ # the Resource or list of Resources that was spliced into the Collection
444
+ # @return [nil]
445
+ # If nil was used to delete the entries
446
+ #
447
+ # @api public
448
+ def []=(*args)
449
+ orphans = Array(superclass_slice(*args[0..-2]))
450
+
451
+ # relate new resources
452
+ resources = resources_added(super)
453
+
454
+ # mark resources as removed
455
+ resources_removed(orphans - loaded_entries)
456
+
457
+ resources
458
+ end
459
+
460
+ alias_method :splice, :[]=
461
+
462
+ # Return a copy of the Collection sorted in reverse
463
+ #
464
+ # @return [Collection]
465
+ # Collection equal to +self+ but ordered in reverse
466
+ #
467
+ # @api public
468
+ def reverse
469
+ dup.reverse!
470
+ end
471
+
472
+ # Return the Collection sorted in reverse
473
+ #
474
+ # @return [self]
475
+ #
476
+ # @api public
477
+ def reverse!
478
+ query.reverse!
479
+
480
+ # reverse without kicking if possible
481
+ if loaded?
482
+ @array.reverse!
483
+ else
484
+ # reverse and swap the head and tail
485
+ @head = tail.reverse!
486
+ @tail = head.reverse!
487
+ end
488
+
489
+ self
490
+ end
491
+
492
+ # Iterate over each Resource
493
+ #
494
+ # @yield [Resource] Each resource in the collection
495
+ #
496
+ # @return [self]
497
+ #
498
+ # @api public
499
+ def each
500
+ return to_enum unless block_given?
501
+
502
+ super do |resource|
503
+ original = resource.collection
504
+ resource.collection = self
505
+ yield resource
506
+ ensure
507
+ resource.collection = original
508
+ end
509
+ end
510
+
511
+ # Invoke the block for each resource and replace it the return value
512
+ #
513
+ # @yield [Resource] Each resource in the collection
514
+ #
515
+ # @return [self]
516
+ #
517
+ # @api public
518
+ def collect!
519
+ super { |resource| resource_added(yield(resource_removed(resource))) }
520
+ end
521
+
522
+ alias_method :map!, :collect!
523
+
524
+ # Append one Resource to the Collection and relate it
525
+ #
526
+ # @param [Resource] resource
527
+ # the resource to add to this collection
528
+ #
529
+ # @return [self]
530
+ #
531
+ # @api public
532
+ def <<(resource)
533
+ super(resource_added(resource))
534
+ end
535
+
536
+ # Appends the resources to self
537
+ #
538
+ # @param [Enumerable] resources
539
+ # List of Resources to append to the collection
540
+ #
541
+ # @return [self]
542
+ #
543
+ # @api public
544
+ def concat(resources)
545
+ super(resources_added(resources))
546
+ end
547
+
548
+ # Append one or more Resources to the Collection
549
+ #
550
+ # This should append one or more Resources to the Collection and
551
+ # relate each to the Collection.
552
+ #
553
+ # @param [Enumerable] *resources
554
+ # List of Resources to append
555
+ #
556
+ # @return [self]
557
+ #
558
+ # @api public
559
+ def push(*resources)
560
+ super(*resources_added(resources))
561
+ end
562
+
563
+ # Prepend one or more Resources to the Collection
564
+ #
565
+ # This should prepend one or more Resources to the Collection and
566
+ # relate each to the Collection.
567
+ #
568
+ # @param [Enumerable] *resources
569
+ # The Resources to prepend
570
+ #
571
+ # @return [self]
572
+ #
573
+ # @api public
574
+ def unshift(*resources)
575
+ super(*resources_added(resources))
576
+ end
577
+
578
+ # Inserts the Resources before the Resource at the offset (which may be negative).
579
+ #
580
+ # @param [Integer] offset
581
+ # The offset to insert the Resources before
582
+ # @param [Enumerable] *resources
583
+ # List of Resources to insert
584
+ #
585
+ # @return [self]
586
+ #
587
+ # @api public
588
+ def insert(offset, *resources)
589
+ super(offset, *resources_added(resources))
590
+ end
591
+
592
+ # Removes and returns the last Resource in the Collection
593
+ #
594
+ # @return [Resource]
595
+ # the last Resource in the Collection
596
+ #
597
+ # @api public
598
+ def pop(*)
599
+ return unless (removed = super)
600
+
601
+ resources_removed(removed)
602
+ end
603
+
604
+ # Removes and returns the first Resource in the Collection
605
+ #
606
+ # @return [Resource]
607
+ # the first Resource in the Collection
608
+ #
609
+ # @api public
610
+ def shift(*)
611
+ return unless (removed = super)
612
+
613
+ resources_removed(removed)
614
+ end
615
+
616
+ # Remove Resource from the Collection
617
+ #
618
+ # This should remove an included Resource from the Collection and
619
+ # orphan it from the Collection. If the Resource is not within the
620
+ # Collection, it should return nil.
621
+ #
622
+ # @param [Resource] resource the Resource to remove from
623
+ # the Collection
624
+ #
625
+ # @return [Resource]
626
+ # If +resource+ is within the Collection
627
+ # @return [nil]
628
+ # If +resource+ is not within the Collection
629
+ #
630
+ # @api public
631
+ def delete(resource)
632
+ return unless (resource = super)
633
+
634
+ resource_removed(resource)
635
+ end
636
+
637
+ # Remove Resource from the Collection by offset
638
+ #
639
+ # This should remove the Resource from the Collection at a given
640
+ # offset and orphan it from the Collection. If the offset is out of
641
+ # range return nil.
642
+ #
643
+ # @param [Integer] offset
644
+ # the offset of the Resource to remove from the Collection
645
+ #
646
+ # @return [Resource]
647
+ # If +offset+ is within the Collection
648
+ # @return [nil]
649
+ # If +offset+ is not within the Collection
650
+ #
651
+ # @api public
652
+ def delete_at(offset)
653
+ return unless (resource = super)
654
+
655
+ resource_removed(resource)
656
+ end
657
+
658
+ # Deletes every Resource for which block evaluates to true.
659
+ #
660
+ # @yield [Resource] Each resource in the Collection
661
+ #
662
+ # @return [self]
663
+ #
664
+ # @api public
665
+ def delete_if
666
+ super { |resource| yield(resource) && resource_removed(resource) }
667
+ end
668
+
669
+ # Deletes every Resource for which block evaluates to true
670
+ #
671
+ # @yield [Resource] Each resource in the Collection
672
+ #
673
+ # @return [Collection]
674
+ # If resources were removed
675
+ # @return [nil]
676
+ # If no resources were removed
677
+ #
678
+ # @api public
679
+ def reject!
680
+ super { |resource| yield(resource) && resource_removed(resource) }
681
+ end
682
+
683
+ # Access LazyArray#replace directly
684
+ #
685
+ # @api private
686
+ alias_method :superclass_replace, :replace
687
+ private :superclass_replace
688
+
689
+ # Replace the Resources within the Collection
690
+ #
691
+ # @param [Enumerable] other
692
+ # List of other Resources to replace with
693
+ #
694
+ # @return [self]
695
+ #
696
+ # @api public
697
+ def replace(other)
698
+ other = resources_added(other)
699
+ resources_removed(entries - other)
700
+ super(other)
701
+ end
702
+
703
+ # (Private) Set the Collection
704
+ #
705
+ # @param [Array] resources
706
+ # resources to add to the collection
707
+ #
708
+ # @return [self]
709
+ #
710
+ # @api private
711
+ def set(resources)
712
+ superclass_replace(resources_added(resources))
713
+ self
714
+ end
715
+
716
+ # Removes all Resources from the Collection
717
+ #
718
+ # This should remove and orphan each Resource from the Collection
719
+ #
720
+ # @return [self]
721
+ #
722
+ # @api public
723
+ def clear
724
+ resources_removed(self) if loaded?
725
+ super
726
+ end
727
+
728
+ # Determines whether the collection is empty.
729
+ #
730
+ # @api public
731
+ alias_method :blank?, :empty?
732
+
733
+ # Finds the first Resource by conditions, or initializes a new
734
+ # Resource with the attributes if none found
735
+ #
736
+ # @param [Hash] conditions
737
+ # The conditions to be used to search
738
+ # @param [Hash] attributes
739
+ # The attributes to be used to initialize the resource with if none found
740
+ # @return [Resource]
741
+ # The instance found by +query+, or created with +attributes+ if none found
742
+ #
743
+ # @api public
744
+ def first_or_new(conditions = {}, attributes = {})
745
+ first(conditions) || new(conditions.merge(attributes))
746
+ end
747
+
748
+ # Finds the first Resource by conditions, or creates a new
749
+ # Resource with the attributes if none found
750
+ #
751
+ # @param [Hash] conditions
752
+ # The conditions to be used to search
753
+ # @param [Hash] attributes
754
+ # The attributes to be used to create the resource with if none found
755
+ # @return [Resource]
756
+ # The instance found by +query+, or created with +attributes+ if none found
757
+ #
758
+ # @api public
759
+ def first_or_create(conditions = {}, attributes = {})
760
+ first(conditions) || create(conditions.merge(attributes))
761
+ end
762
+
763
+ # Initializes a Resource and appends it to the Collection
764
+ #
765
+ # @param [Hash] attributes
766
+ # Attributes with which to initialize the new resource
767
+ #
768
+ # @return [Resource]
769
+ # a new Resource initialized with +attributes+
770
+ #
771
+ # @api public
772
+ def new(attributes = {})
773
+ resource = repository.scope { model.new(attributes) }
774
+ self << resource
775
+ resource
776
+ end
777
+
778
+ # Create a Resource in the Collection
779
+ #
780
+ # @param [Hash(Symbol => Object)] attributes
781
+ # attributes to set
782
+ #
783
+ # @return [Resource]
784
+ # the newly created Resource instance
785
+ #
786
+ # @api public
787
+ def create(attributes = {})
788
+ _create(attributes)
789
+ end
790
+
791
+ # Create a Resource in the Collection, bypassing hooks
792
+ #
793
+ # @param [Hash(Symbol => Object)] attributes
794
+ # attributes to set
795
+ #
796
+ # @return [Resource]
797
+ # the newly created Resource instance
798
+ #
799
+ # @api public
800
+ def create!(attributes = {})
801
+ _create(attributes, false)
802
+ end
803
+
804
+ # Update every Resource in the Collection
805
+ #
806
+ # Person.all(:age.gte => 21).update(:allow_beer => true)
807
+ #
808
+ # @param [Hash] attributes
809
+ # attributes to update with
810
+ #
811
+ # @return [Boolean]
812
+ # true if the resources were successfully updated
813
+ #
814
+ # @api public
815
+ def update(attributes)
816
+ assert_update_clean_only(:update)
817
+
818
+ dirty_attributes = model.new(attributes).dirty_attributes
819
+ dirty_attributes.empty? || all? { |resource| resource.update(attributes) }
820
+ end
821
+
822
+ # Update every Resource in the Collection bypassing validation
823
+ #
824
+ # Person.all(:age.gte => 21).update!(:allow_beer => true)
825
+ #
826
+ # @param [Hash] attributes
827
+ # attributes to update
828
+ #
829
+ # @return [Boolean]
830
+ # true if the resources were successfully updated
831
+ #
832
+ # @api public
833
+ def update!(attributes)
834
+ assert_update_clean_only(:update!)
835
+
836
+ model = self.model
837
+
838
+ dirty_attributes = model.new(attributes).dirty_attributes
839
+
840
+ unless dirty_attributes.empty?
841
+ dirty_attributes.each do |property, value|
842
+ property.assert_valid_value(value)
843
+ end
844
+ return false unless _update(dirty_attributes)
845
+
846
+ if loaded?
847
+ each do |resource|
848
+ dirty_attributes.each { |property, value| property.set!(resource, value) }
849
+ repository.identity_map(model)[resource.key] = resource
850
+ end
851
+ end
852
+ end
853
+ true
854
+ end
855
+
856
+ # Save every Resource in the Collection
857
+ #
858
+ # @return [Boolean]
859
+ # true if the resources were successfully saved
860
+ #
861
+ # @api public
862
+ def save
863
+ _save
864
+ end
865
+
866
+ # Save every Resource in the Collection bypassing validation
867
+ #
868
+ # @return [Boolean]
869
+ # true if the resources were successfully saved
870
+ #
871
+ # @api public
872
+ def save!
873
+ _save(false)
874
+ end
875
+
876
+ # Remove every Resource in the Collection from the repository
877
+ #
878
+ # This performs a deletion of each Resource in the Collection from
879
+ # the repository and clears the Collection.
880
+ #
881
+ # @return [Boolean]
882
+ # true if the resources were successfully destroyed
883
+ #
884
+ # @api public
885
+ def destroy
886
+ if (destroyed = all?(&:destroy))
887
+ clear
888
+ end
889
+
890
+ destroyed
891
+ end
892
+
893
+ # Remove all Resources from the repository, bypassing validation
894
+ #
895
+ # This performs a deletion of each Resource in the Collection from
896
+ # the repository and clears the Collection while skipping
897
+ # validation.
898
+ #
899
+ # @return [Boolean]
900
+ # true if the resources were successfully destroyed
901
+ #
902
+ # @api public
903
+ def destroy!
904
+ repository = self.repository
905
+ deleted = repository.delete(self)
906
+
907
+ if loaded?
908
+ return false unless deleted == size
909
+
910
+ each do |resource|
911
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
912
+ end
913
+
914
+ clear
915
+ else
916
+ mark_loaded
917
+ end
918
+
919
+ true
920
+ end
921
+
922
+ # Check to see if collection can respond to the method
923
+ #
924
+ # @param [Symbol] method
925
+ # method to check in the object
926
+ # @param [Boolean] include_private
927
+ # if set to true, collection will check private methods
928
+ #
929
+ # @return [Boolean]
930
+ # true if method can be responded to
931
+ #
932
+ # @api public
933
+ def respond_to?(method, include_private = false)
934
+ super || model.respond_to?(method) || relationships.named?(method)
935
+ end
936
+
937
+ # Checks if all the resources have no changes to save
938
+ #
939
+ # @return [Boolean]
940
+ # true if the resource may not be persisted
941
+ #
942
+ # @api public
943
+ def clean?
944
+ !dirty?
945
+ end
946
+
947
+ # Checks if any resources have unsaved changes
948
+ #
949
+ # @return [Boolean]
950
+ # true if the resources have unsaved changed
951
+ #
952
+ # @api public
953
+ def dirty?
954
+ loaded_entries.any?(&:dirty?) || @removed.any?
955
+ end
956
+
957
+ # Gets a Human-readable representation of this collection,
958
+ # showing all elements contained in it
959
+ #
960
+ # @return [String]
961
+ # Human-readable representation of this collection, showing all elements
962
+ #
963
+ # @api public
964
+ def inspect
965
+ "[#{map(&:inspect).join(', ')}]"
966
+ end
967
+
968
+ # @api semipublic
969
+ def hash
970
+ [self.class, query].hash
971
+ end
972
+
973
+ # Returns the model key
974
+ #
975
+ # @return [PropertySet]
976
+ # the model key
977
+ #
978
+ # @api private
979
+ protected def model_key
980
+ model.key(repository_name)
981
+ end
982
+
983
+ # Loaded Resources in the collection
984
+ #
985
+ # @return [Array<Resource>]
986
+ # Resources in the collection
987
+ #
988
+ # @api private
989
+ protected def loaded_entries
990
+ (loaded? ? self : head + tail).reject(&:destroyed?)
991
+ end
992
+
993
+ # Returns the PropertySet representing the fields in the Collection scope
994
+ #
995
+ # @return [PropertySet]
996
+ # The set of properties this Collection's query will retrieve
997
+ #
998
+ # @api private
999
+ protected def properties
1000
+ model.properties(repository_name)
1001
+ end
1002
+
1003
+ # Returns the Relationships for the Collection's Model
1004
+ #
1005
+ # @return [Hash]
1006
+ # The model's relationships, mapping the name to the
1007
+ # Associations::Relationship object
1008
+ #
1009
+ # @api private
1010
+ protected def relationships
1011
+ model.relationships(repository_name)
1012
+ end
1013
+
1014
+ # Initializes a new Collection identified by the query
1015
+ #
1016
+ # @param [Query] query
1017
+ # Scope the results of the Collection
1018
+ # @param [Enumerable] resources (optional)
1019
+ # List of resources to initialize the Collection with
1020
+ #
1021
+ # @return [self]
1022
+ #
1023
+ # @api private
1024
+ private def initialize(query, resources = nil)
1025
+ raise "#{self.class}#new with a block is deprecated" if block_given?
1026
+
1027
+ @query = query
1028
+ @identity_map = IdentityMap.new
1029
+ @removed = Set.new
1030
+
1031
+ super()
1032
+
1033
+ # TODO: change LazyArray to not use a load proc at all
1034
+ remove_instance_variable(:@load_with_proc)
1035
+
1036
+ set(resources) if resources
1037
+ end
1038
+
1039
+ # Copies the original Collection state
1040
+ #
1041
+ # @param [Collection] original
1042
+ # the original collection to copy from
1043
+ #
1044
+ # @return [undefined]
1045
+ #
1046
+ # @api private
1047
+ private def initialize_copy(original)
1048
+ super
1049
+ @query = @query.dup
1050
+ @identity_map = @identity_map.dup
1051
+ @removed = @removed.dup
1052
+ end
1053
+
1054
+ # Initialize a resource from a Hash
1055
+ #
1056
+ # @param [Resource, Hash] resource
1057
+ # resource to process
1058
+ #
1059
+ # @return [Resource]
1060
+ # an initialized resource
1061
+ #
1062
+ # @api private
1063
+ private def initialize_resource(resource)
1064
+ resource.is_a?(Hash) ? new(resource) : resource
1065
+ end
1066
+
1067
+ # Test if the collection is loaded between the offset and limit
1068
+ #
1069
+ # @param [Integer] offset
1070
+ # the offset of the collection to test
1071
+ # @param [Integer] limit
1072
+ # optional limit for how many entries to be loaded
1073
+ #
1074
+ # @return [Boolean]
1075
+ # true if the collection is loaded from the offset to the limit
1076
+ #
1077
+ # @api private
1078
+ private def partially_loaded?(offset, limit = 1)
1079
+ if offset >= 0
1080
+ lazy_possible?(head, offset + limit)
1081
+ else
1082
+ lazy_possible?(tail, offset.abs)
1083
+ end
1084
+ end
1085
+
1086
+ # Lazy loads a Collection
1087
+ #
1088
+ # @return [self]
1089
+ #
1090
+ # @api private
1091
+ private def lazy_load
1092
+ return self if loaded?
1093
+
1094
+ mark_loaded
1095
+
1096
+ head = self.head
1097
+ tail = self.tail
1098
+ query = self.query
1099
+
1100
+ resources = repository.read(query)
1101
+
1102
+ # remove already known results
1103
+ resources -= head if head.any?
1104
+ resources -= tail if tail.any?
1105
+ resources -= @removed.to_a if @removed.any?
1106
+
1107
+ query.add_reversed? ? unshift(*resources.reverse) : concat(resources)
1108
+
1109
+ # TODO: DRY this up with LazyArray
1110
+ @array.unshift(*head)
1111
+ @array.concat(tail)
1112
+
1113
+ @head = @tail = nil
1114
+ @reapers&.each { |resource| @array.delete_if(&resource) }
1115
+ @array.freeze if frozen?
1116
+
1117
+ self
1118
+ end
1119
+
1120
+ # Returns the Query Repository name
1121
+ #
1122
+ # @return [Symbol]
1123
+ # the repository name
1124
+ #
1125
+ # @api private
1126
+ private def repository_name
1127
+ repository.name
1128
+ end
1129
+
1130
+ # Initializes a new Collection
1131
+ #
1132
+ # @return [Collection]
1133
+ # A new Collection object
1134
+ #
1135
+ # @api private
1136
+ private def new_collection(query, resources = nil, &block)
1137
+ resources ||= filter(query) if loaded?
1138
+
1139
+ # TOOD: figure out a way to pass not-yet-saved Resources to this newly
1140
+ # created Collection. If the new resource matches the conditions, then
1141
+ # it should be added to the collection (keep in mind limit/offset too)
1142
+
1143
+ self.class.new(query, resources, &block)
1144
+ end
1145
+
1146
+ # Apply a set operation on self and another collection
1147
+ #
1148
+ # @param [Symbol] operation
1149
+ # the set operation to apply
1150
+ # @param [Collection] other
1151
+ # the other collection to apply the set operation on
1152
+ #
1153
+ # @return [Collection]
1154
+ # the collection that was created for the set operation
1155
+ #
1156
+ # @api private
1157
+ private def set_operation(operation, other)
1158
+ resources = set_operation_resources(operation, other)
1159
+ other_query = Query.target_query(repository, model, other)
1160
+ new_collection(query.send(operation, other_query), resources)
1161
+ end
1162
+
1163
+ # Prepopulate the set operation if the collection is loaded
1164
+ #
1165
+ # @param [Symbol] operation
1166
+ # the set operation to apply
1167
+ # @param [Collection] other
1168
+ # the other collection to apply the set operation on
1169
+ #
1170
+ # @return [nil]
1171
+ # nil if the Collection is not loaded
1172
+ # @return [Array]
1173
+ # the resources to prepopulate the set operation results with
1174
+ #
1175
+ # @api private
1176
+ private def set_operation_resources(operation, other)
1177
+ entries.send(operation, other.entries) if loaded?
1178
+ end
1179
+
1180
+ # Creates a resource in the collection
1181
+ #
1182
+ # @param [Boolean] execute_hooks
1183
+ # Whether to execute hooks or not
1184
+ # @param [Hash] attributes
1185
+ # Attributes with which to create the new resource
1186
+ #
1187
+ # @return [Resource]
1188
+ # a saved Resource
1189
+ #
1190
+ # @api private
1191
+ private def _create(attributes, execute_hooks = true)
1192
+ resource = repository.scope { model.send(execute_hooks ? :create : :create!, default_attributes.merge(attributes)) }
1193
+ self << resource if resource&.saved?
1194
+ resource
1195
+ end
1196
+
1197
+ # Updates a collection
1198
+ #
1199
+ # @return [Boolean]
1200
+ # Returns true if collection was updated
1201
+ #
1202
+ # @api private
1203
+ private def _update(dirty_attributes)
1204
+ repository.update(dirty_attributes, self)
1205
+ true
1206
+ end
1207
+
1208
+ # Saves a collection
1209
+ #
1210
+ # @param [Boolean] execute_hooks
1211
+ # Whether to execute hooks or not
1212
+ #
1213
+ # @return [Boolean]
1214
+ # Returns true if collection was updated
1215
+ #
1216
+ # @api private
1217
+ private def _save(execute_hooks = true)
1218
+ loaded_entries = self.loaded_entries
1219
+ loaded_entries.each { |resource| set_default_attributes(resource) }
1220
+ @removed.clear
1221
+ loaded_entries.all? { |resource| resource.__send__(execute_hooks ? :save : :save!) }
1222
+ end
1223
+
1224
+ # Returns default values to initialize new Resources in the Collection
1225
+ #
1226
+ # @return [Hash] The default attributes for new instances in this Collection
1227
+ #
1228
+ # @api private
1229
+ private def default_attributes
1230
+ return @default_attributes if @default_attributes
1231
+
1232
+ default_attributes = {}
1233
+
1234
+ conditions = query.conditions
1235
+
1236
+ if conditions.slug == :and
1237
+ model_properties = properties.dup
1238
+ model_key = self.model_key
1239
+
1240
+ model_properties -= model_key if model_properties.to_set.superset?(model_key.to_set)
1241
+
1242
+ conditions.each do |condition|
1243
+ next unless condition.slug == :eql
1244
+
1245
+ subject = condition.subject
1246
+ next unless model_properties.include?(subject) || (condition.relationship? && subject.source_model == model)
1247
+
1248
+ default_attributes[subject] = condition.loaded_value
1249
+ end
1250
+ end
1251
+
1252
+ @default_attributes = default_attributes.freeze
1253
+ end
1254
+
1255
+ # Set the default attributes for a non-frozen resource
1256
+ #
1257
+ # @param [Resource] resource
1258
+ # the resource to set the default attributes for
1259
+ #
1260
+ # @return [undefined]
1261
+ #
1262
+ # @api private
1263
+ private def set_default_attributes(resource)
1264
+ resource.attributes = default_attributes unless resource.readonly?
1265
+ end
1266
+
1267
+ # Track the added resource
1268
+ #
1269
+ # @param [Resource] resource
1270
+ # the resource that was added
1271
+ #
1272
+ # @return [Resource]
1273
+ # the resource that was added
1274
+ #
1275
+ # @api private
1276
+ private def resource_added(resource)
1277
+ resource = initialize_resource(resource)
1278
+
1279
+ if resource&.saved?
1280
+ @identity_map[resource.key] = resource
1281
+ @removed.delete(resource)
1282
+ else
1283
+ set_default_attributes(resource)
1284
+ end
1285
+
1286
+ resource
1287
+ end
1288
+
1289
+ # Track the added resources
1290
+ #
1291
+ # @param [Array<Resource>] resources
1292
+ # the resources that were added
1293
+ #
1294
+ # @return [Array<Resource>]
1295
+ # the resources that were added
1296
+ #
1297
+ # @api private
1298
+ private def resources_added(resources)
1299
+ if resources.is_a?(Enumerable)
1300
+ resources.map { |resource| resource_added(resource) }
1301
+ else
1302
+ resource_added(resources)
1303
+ end
1304
+ end
1305
+
1306
+ # Track the removed resource
1307
+ #
1308
+ # @param [Resource] resource
1309
+ # the resource that was removed
1310
+ #
1311
+ # @return [Resource]
1312
+ # the resource that was removed
1313
+ #
1314
+ # @api private
1315
+ private def resource_removed(resource)
1316
+ if resource.saved?
1317
+ @identity_map.delete(resource.key)
1318
+ @removed << resource
1319
+ end
1320
+
1321
+ resource
1322
+ end
1323
+
1324
+ # Track the removed resources
1325
+ #
1326
+ # @param [Array<Resource>] resources
1327
+ # the resources that were removed
1328
+ #
1329
+ # @return [Array<Resource>]
1330
+ # the resources that were removed
1331
+ #
1332
+ # @api private
1333
+ private def resources_removed(resources)
1334
+ if resources.is_a?(Enumerable)
1335
+ resources.each { |resource| resource_removed(resource) }
1336
+ else
1337
+ resource_removed(resources)
1338
+ end
1339
+ end
1340
+
1341
+ # Filter resources in the collection based on a Query
1342
+ #
1343
+ # @param [Query] other_query
1344
+ # the query to match each resource in the collection
1345
+ #
1346
+ # @return [Array]
1347
+ # the resources that match the Query
1348
+ # @return [nil]
1349
+ # nil if no resources match the Query
1350
+ #
1351
+ # @api private
1352
+ private def filter(other_query)
1353
+ query = self.query
1354
+ fields = query.fields.to_set
1355
+ unique = other_query.unique?
1356
+
1357
+ # TODO: push this into a Query#subset? method
1358
+ other_query.filter_records(to_a.dup) if other_query.links.empty? &&
1359
+ (unique || (!unique && !query.unique?)) &&
1360
+ !other_query.reload? &&
1361
+ !other_query.raw? &&
1362
+ other_query.fields.to_set.subset?(fields) &&
1363
+ other_query.condition_properties.subset?(fields)
1364
+ end
1365
+
1366
+ # Return the absolute or relative scoped query
1367
+ #
1368
+ # @param [Query, Hash] query
1369
+ # the query to scope the collection with
1370
+ #
1371
+ # @return [Query]
1372
+ # the absolute or relative scoped query
1373
+ #
1374
+ # @api private
1375
+ private def scoped_query(query)
1376
+ if query.is_a?(Query)
1377
+ query.dup
1378
+ else
1379
+ self.query.relative(query)
1380
+ end
1381
+ end
1382
+
1383
+ # @api private
1384
+ private def sliced_query(offset, limit)
1385
+ query = self.query
1386
+
1387
+ if offset >= 0
1388
+ query.slice(offset, limit)
1389
+ else
1390
+ query = query.slice((limit + offset).abs, limit).reverse!
1391
+
1392
+ # tell the Query to prepend each result from the adapter
1393
+ query.update(add_reversed: !query.add_reversed?)
1394
+ end
1395
+ end
1396
+
1397
+ # Delegates to Model, Relationships or the superclass (LazyArray)
1398
+ #
1399
+ # When this receives a method that belongs to the Model the
1400
+ # Collection is scoped to, it will execute the method within the
1401
+ # same scope as the Collection and return the results.
1402
+ #
1403
+ # When this receives a method that is a relationship the Model has
1404
+ # defined, it will execute the association method within the same
1405
+ # scope as the Collection and return the results.
1406
+ #
1407
+ # Otherwise this method will delegate to a method in the superclass
1408
+ # (LazyArray) and return the results.
1409
+ #
1410
+ # @return [Object]
1411
+ # the return values of the delegated methods
1412
+ #
1413
+ # @api public
1414
+ private def method_missing(method, *args, &block)
1415
+ relationships = self.relationships
1416
+
1417
+ if model.respond_to?(method)
1418
+ delegate_to_model(method, *args, &block)
1419
+ elsif (relationship = relationships[method]) || relationships[DataMapper::Inflector.singularize(method.to_s).to_sym]
1420
+ delegate_to_relationship(relationship, *args)
1421
+ else
1422
+ super
1423
+ end
1424
+ end
1425
+
1426
+ # Delegate the method to the Model
1427
+ #
1428
+ # @param [Symbol] method
1429
+ # the name of the method in the model to execute
1430
+ # @param [Array] *args
1431
+ # the arguments for the method
1432
+ #
1433
+ # @return [Object]
1434
+ # the return value of the model method
1435
+ #
1436
+ # @api private
1437
+ private def delegate_to_model(method, *args, &block)
1438
+ model = self.model
1439
+ model.send(:with_scope, query) do
1440
+ model.send(method, *args, &block)
1441
+ end
1442
+ end
1443
+
1444
+ # Delegate the method to the Relationship
1445
+ #
1446
+ # @return [Collection]
1447
+ # the associated Resources
1448
+ #
1449
+ # @api private
1450
+ private def delegate_to_relationship(relationship, query = nil)
1451
+ relationship.eager_load(self, query)
1452
+ end
1453
+
1454
+ # Raises an exception if #update is performed on a dirty resource
1455
+ #
1456
+ # @raise [UpdateConflictError]
1457
+ # raise if the resource is dirty
1458
+ #
1459
+ # @return [undefined]
1460
+ #
1461
+ # @api private
1462
+ private def assert_update_clean_only(method)
1463
+ raise UpdateConflictError, "#{self.class}##{method} cannot be called on a dirty collection" if dirty?
1464
+ end
1465
+
1466
+ # Raises an exception if #get receives the wrong number of arguments
1467
+ #
1468
+ # @param [Array] key
1469
+ # the key value
1470
+ #
1471
+ # @return [undefined]
1472
+ #
1473
+ # @raise [UpdateConflictError]
1474
+ # raise if the resource is dirty
1475
+ #
1476
+ # @api private
1477
+ private def assert_valid_key_size(key)
1478
+ expected_key_size = model_key.size
1479
+ actual_key_size = key.size
1480
+
1481
+ return unless actual_key_size != expected_key_size
1482
+
1483
+ raise ArgumentError, "The number of arguments for the key is invalid, expected #{expected_key_size} but was #{actual_key_size}"
1484
+ end
1485
+ end
1486
+ end