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