ardm-core 1.2.1

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