ghost_dm-core 1.3.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +35 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +65 -0
  6. data/LICENSE +20 -0
  7. data/README.md +269 -0
  8. data/Rakefile +4 -0
  9. data/dm-core.gemspec +24 -0
  10. data/lib/dm-core.rb +292 -0
  11. data/lib/dm-core/adapters.rb +222 -0
  12. data/lib/dm-core/adapters/abstract_adapter.rb +237 -0
  13. data/lib/dm-core/adapters/in_memory_adapter.rb +113 -0
  14. data/lib/dm-core/associations/many_to_many.rb +499 -0
  15. data/lib/dm-core/associations/many_to_one.rb +290 -0
  16. data/lib/dm-core/associations/one_to_many.rb +348 -0
  17. data/lib/dm-core/associations/one_to_one.rb +86 -0
  18. data/lib/dm-core/associations/relationship.rb +663 -0
  19. data/lib/dm-core/backwards.rb +13 -0
  20. data/lib/dm-core/collection.rb +1515 -0
  21. data/lib/dm-core/core_ext/kernel.rb +23 -0
  22. data/lib/dm-core/core_ext/pathname.rb +6 -0
  23. data/lib/dm-core/core_ext/symbol.rb +10 -0
  24. data/lib/dm-core/identity_map.rb +7 -0
  25. data/lib/dm-core/model.rb +874 -0
  26. data/lib/dm-core/model/hook.rb +103 -0
  27. data/lib/dm-core/model/is.rb +32 -0
  28. data/lib/dm-core/model/property.rb +249 -0
  29. data/lib/dm-core/model/relationship.rb +378 -0
  30. data/lib/dm-core/model/scope.rb +89 -0
  31. data/lib/dm-core/property.rb +866 -0
  32. data/lib/dm-core/property/binary.rb +21 -0
  33. data/lib/dm-core/property/boolean.rb +20 -0
  34. data/lib/dm-core/property/class.rb +17 -0
  35. data/lib/dm-core/property/date.rb +10 -0
  36. data/lib/dm-core/property/date_time.rb +10 -0
  37. data/lib/dm-core/property/decimal.rb +36 -0
  38. data/lib/dm-core/property/discriminator.rb +44 -0
  39. data/lib/dm-core/property/float.rb +16 -0
  40. data/lib/dm-core/property/integer.rb +22 -0
  41. data/lib/dm-core/property/invalid_value_error.rb +22 -0
  42. data/lib/dm-core/property/lookup.rb +27 -0
  43. data/lib/dm-core/property/numeric.rb +38 -0
  44. data/lib/dm-core/property/object.rb +34 -0
  45. data/lib/dm-core/property/serial.rb +14 -0
  46. data/lib/dm-core/property/string.rb +38 -0
  47. data/lib/dm-core/property/text.rb +9 -0
  48. data/lib/dm-core/property/time.rb +10 -0
  49. data/lib/dm-core/property_set.rb +177 -0
  50. data/lib/dm-core/query.rb +1366 -0
  51. data/lib/dm-core/query/conditions/comparison.rb +911 -0
  52. data/lib/dm-core/query/conditions/operation.rb +721 -0
  53. data/lib/dm-core/query/direction.rb +36 -0
  54. data/lib/dm-core/query/operator.rb +35 -0
  55. data/lib/dm-core/query/path.rb +114 -0
  56. data/lib/dm-core/query/sort.rb +39 -0
  57. data/lib/dm-core/relationship_set.rb +72 -0
  58. data/lib/dm-core/repository.rb +226 -0
  59. data/lib/dm-core/resource.rb +1214 -0
  60. data/lib/dm-core/resource/persistence_state.rb +75 -0
  61. data/lib/dm-core/resource/persistence_state/clean.rb +40 -0
  62. data/lib/dm-core/resource/persistence_state/deleted.rb +30 -0
  63. data/lib/dm-core/resource/persistence_state/dirty.rb +96 -0
  64. data/lib/dm-core/resource/persistence_state/immutable.rb +34 -0
  65. data/lib/dm-core/resource/persistence_state/persisted.rb +29 -0
  66. data/lib/dm-core/resource/persistence_state/transient.rb +80 -0
  67. data/lib/dm-core/spec/lib/adapter_helpers.rb +64 -0
  68. data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
  69. data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
  70. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  71. data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
  72. data/lib/dm-core/spec/setup.rb +174 -0
  73. data/lib/dm-core/spec/shared/adapter_spec.rb +341 -0
  74. data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
  75. data/lib/dm-core/spec/shared/resource_spec.rb +1232 -0
  76. data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
  77. data/lib/dm-core/spec/shared/semipublic/property_spec.rb +176 -0
  78. data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
  79. data/lib/dm-core/support/assertions.rb +8 -0
  80. data/lib/dm-core/support/chainable.rb +18 -0
  81. data/lib/dm-core/support/deprecate.rb +12 -0
  82. data/lib/dm-core/support/descendant_set.rb +89 -0
  83. data/lib/dm-core/support/equalizer.rb +48 -0
  84. data/lib/dm-core/support/ext/array.rb +22 -0
  85. data/lib/dm-core/support/ext/blank.rb +25 -0
  86. data/lib/dm-core/support/ext/hash.rb +67 -0
  87. data/lib/dm-core/support/ext/module.rb +47 -0
  88. data/lib/dm-core/support/ext/object.rb +57 -0
  89. data/lib/dm-core/support/ext/string.rb +24 -0
  90. data/lib/dm-core/support/ext/try_dup.rb +12 -0
  91. data/lib/dm-core/support/hook.rb +405 -0
  92. data/lib/dm-core/support/inflections.rb +60 -0
  93. data/lib/dm-core/support/inflector/inflections.rb +211 -0
  94. data/lib/dm-core/support/inflector/methods.rb +151 -0
  95. data/lib/dm-core/support/lazy_array.rb +451 -0
  96. data/lib/dm-core/support/local_object_space.rb +13 -0
  97. data/lib/dm-core/support/logger.rb +201 -0
  98. data/lib/dm-core/support/mash.rb +176 -0
  99. data/lib/dm-core/support/naming_conventions.rb +90 -0
  100. data/lib/dm-core/support/ordered_set.rb +380 -0
  101. data/lib/dm-core/support/subject.rb +33 -0
  102. data/lib/dm-core/support/subject_set.rb +250 -0
  103. data/lib/dm-core/version.rb +3 -0
  104. data/script/performance.rb +275 -0
  105. data/script/profile.rb +218 -0
  106. data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
  107. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +68 -0
  108. data/spec/public/associations/many_to_many_spec.rb +197 -0
  109. data/spec/public/associations/many_to_one_spec.rb +83 -0
  110. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
  111. data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
  112. data/spec/public/associations/one_to_many_spec.rb +81 -0
  113. data/spec/public/associations/one_to_one_spec.rb +176 -0
  114. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
  115. data/spec/public/collection_spec.rb +69 -0
  116. data/spec/public/finalize_spec.rb +76 -0
  117. data/spec/public/model/hook_spec.rb +246 -0
  118. data/spec/public/model/property_spec.rb +88 -0
  119. data/spec/public/model/relationship_spec.rb +1040 -0
  120. data/spec/public/model_spec.rb +462 -0
  121. data/spec/public/property/binary_spec.rb +41 -0
  122. data/spec/public/property/boolean_spec.rb +22 -0
  123. data/spec/public/property/class_spec.rb +28 -0
  124. data/spec/public/property/date_spec.rb +22 -0
  125. data/spec/public/property/date_time_spec.rb +22 -0
  126. data/spec/public/property/decimal_spec.rb +23 -0
  127. data/spec/public/property/discriminator_spec.rb +135 -0
  128. data/spec/public/property/float_spec.rb +22 -0
  129. data/spec/public/property/integer_spec.rb +22 -0
  130. data/spec/public/property/object_spec.rb +107 -0
  131. data/spec/public/property/serial_spec.rb +22 -0
  132. data/spec/public/property/string_spec.rb +22 -0
  133. data/spec/public/property/text_spec.rb +63 -0
  134. data/spec/public/property/time_spec.rb +22 -0
  135. data/spec/public/property_spec.rb +341 -0
  136. data/spec/public/resource_spec.rb +288 -0
  137. data/spec/public/sel_spec.rb +53 -0
  138. data/spec/public/setup_spec.rb +145 -0
  139. data/spec/public/shared/association_collection_shared_spec.rb +309 -0
  140. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  141. data/spec/public/shared/collection_shared_spec.rb +1667 -0
  142. data/spec/public/shared/finder_shared_spec.rb +1629 -0
  143. data/spec/rcov.opts +6 -0
  144. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  145. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
  146. data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
  147. data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
  148. data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
  149. data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
  150. data/spec/semipublic/associations/relationship_spec.rb +200 -0
  151. data/spec/semipublic/associations_spec.rb +177 -0
  152. data/spec/semipublic/collection_spec.rb +110 -0
  153. data/spec/semipublic/model_spec.rb +96 -0
  154. data/spec/semipublic/property/binary_spec.rb +13 -0
  155. data/spec/semipublic/property/boolean_spec.rb +47 -0
  156. data/spec/semipublic/property/class_spec.rb +33 -0
  157. data/spec/semipublic/property/date_spec.rb +43 -0
  158. data/spec/semipublic/property/date_time_spec.rb +46 -0
  159. data/spec/semipublic/property/decimal_spec.rb +83 -0
  160. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  161. data/spec/semipublic/property/float_spec.rb +82 -0
  162. data/spec/semipublic/property/integer_spec.rb +82 -0
  163. data/spec/semipublic/property/lookup_spec.rb +29 -0
  164. data/spec/semipublic/property/serial_spec.rb +13 -0
  165. data/spec/semipublic/property/string_spec.rb +13 -0
  166. data/spec/semipublic/property/text_spec.rb +31 -0
  167. data/spec/semipublic/property/time_spec.rb +50 -0
  168. data/spec/semipublic/property_spec.rb +114 -0
  169. data/spec/semipublic/query/conditions/comparison_spec.rb +1501 -0
  170. data/spec/semipublic/query/conditions/operation_spec.rb +1294 -0
  171. data/spec/semipublic/query/path_spec.rb +471 -0
  172. data/spec/semipublic/query_spec.rb +3682 -0
  173. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  174. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  175. data/spec/semipublic/resource/state/dirty_spec.rb +162 -0
  176. data/spec/semipublic/resource/state/immutable_spec.rb +105 -0
  177. data/spec/semipublic/resource/state/transient_spec.rb +162 -0
  178. data/spec/semipublic/resource/state_spec.rb +230 -0
  179. data/spec/semipublic/resource_spec.rb +23 -0
  180. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  181. data/spec/semipublic/shared/resource_shared_spec.rb +199 -0
  182. data/spec/semipublic/shared/resource_state_shared_spec.rb +79 -0
  183. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  184. data/spec/spec.opts +5 -0
  185. data/spec/spec_helper.rb +38 -0
  186. data/spec/support/core_ext/hash.rb +10 -0
  187. data/spec/support/core_ext/inheritable_attributes.rb +46 -0
  188. data/spec/support/properties/huge_integer.rb +17 -0
  189. data/spec/unit/array_spec.rb +23 -0
  190. data/spec/unit/blank_spec.rb +73 -0
  191. data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
  192. data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
  193. data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
  194. data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
  195. data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
  196. data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
  197. data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
  198. data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
  199. data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
  200. data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
  201. data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
  202. data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
  203. data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
  204. data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
  205. data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
  206. data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
  207. data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
  208. data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
  209. data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
  210. data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
  211. data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
  212. data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
  213. data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
  214. data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
  215. data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
  216. data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
  217. data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
  218. data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
  219. data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
  220. data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
  221. data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
  222. data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
  223. data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
  224. data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
  225. data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
  226. data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
  227. data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
  228. data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
  229. data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
  230. data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
  231. data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
  232. data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
  233. data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
  234. data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
  235. data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
  236. data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
  237. data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
  238. data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
  239. data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
  240. data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
  241. data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
  242. data/spec/unit/hash_spec.rb +28 -0
  243. data/spec/unit/hook_spec.rb +1235 -0
  244. data/spec/unit/inflections_spec.rb +16 -0
  245. data/spec/unit/lazy_array_spec.rb +1949 -0
  246. data/spec/unit/mash_spec.rb +312 -0
  247. data/spec/unit/module_spec.rb +71 -0
  248. data/spec/unit/object_spec.rb +38 -0
  249. data/spec/unit/try_dup_spec.rb +46 -0
  250. data/tasks/ci.rake +1 -0
  251. data/tasks/spec.rake +38 -0
  252. data/tasks/yard.rake +9 -0
  253. data/tasks/yardstick.rake +19 -0
  254. metadata +365 -0
@@ -0,0 +1,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,1515 @@
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
+ else
848
+ dirty_attributes.each do |property, value|
849
+ property.assert_valid_value(value)
850
+ end
851
+ unless _update(dirty_attributes)
852
+ return false
853
+ end
854
+
855
+ if loaded?
856
+ each do |resource|
857
+ dirty_attributes.each { |property, value| property.set!(resource, value) }
858
+ repository.identity_map(model)[resource.key] = resource
859
+ end
860
+ end
861
+
862
+ true
863
+ end
864
+ end
865
+
866
+ # Save every Resource in the Collection
867
+ #
868
+ # @return [Boolean]
869
+ # true if the resources were successfully saved
870
+ #
871
+ # @api public
872
+ def save
873
+ _save
874
+ end
875
+
876
+ # Save every Resource in the Collection bypassing validation
877
+ #
878
+ # @return [Boolean]
879
+ # true if the resources were successfully saved
880
+ #
881
+ # @api public
882
+ def save!
883
+ _save(false)
884
+ end
885
+
886
+ # Remove every Resource in the Collection from the repository
887
+ #
888
+ # This performs a deletion of each Resource in the Collection from
889
+ # the repository and clears the Collection.
890
+ #
891
+ # @return [Boolean]
892
+ # true if the resources were successfully destroyed
893
+ #
894
+ # @api public
895
+ def destroy
896
+ if destroyed = all? { |resource| resource.destroy }
897
+ clear
898
+ end
899
+
900
+ destroyed
901
+ end
902
+
903
+ # Remove all Resources from the repository, bypassing validation
904
+ #
905
+ # This performs a deletion of each Resource in the Collection from
906
+ # the repository and clears the Collection while skipping
907
+ # validation.
908
+ #
909
+ # @return [Boolean]
910
+ # true if the resources were successfully destroyed
911
+ #
912
+ # @api public
913
+ def destroy!
914
+ repository = self.repository
915
+ deleted = repository.delete(self)
916
+
917
+ if loaded?
918
+ unless deleted == size
919
+ return false
920
+ end
921
+
922
+ each do |resource|
923
+ resource.persistence_state = Resource::PersistenceState::Immutable.new(resource)
924
+ end
925
+
926
+ clear
927
+ else
928
+ mark_loaded
929
+ end
930
+
931
+ true
932
+ end
933
+
934
+ # Check to see if collection can respond to the method
935
+ #
936
+ # @param [Symbol] method
937
+ # method to check in the object
938
+ # @param [Boolean] include_private
939
+ # if set to true, collection will check private methods
940
+ #
941
+ # @return [Boolean]
942
+ # true if method can be responded to
943
+ #
944
+ # @api public
945
+ def respond_to?(method, include_private = false)
946
+ super || model.respond_to?(method) || relationships.named?(method)
947
+ end
948
+
949
+ # Checks if all the resources have no changes to save
950
+ #
951
+ # @return [Boolean]
952
+ # true if the resource may not be persisted
953
+ #
954
+ # @api public
955
+ def clean?
956
+ !dirty?
957
+ end
958
+
959
+ # Checks if any resources have unsaved changes
960
+ #
961
+ # @return [Boolean]
962
+ # true if the resources have unsaved changed
963
+ #
964
+ # @api public
965
+ def dirty?
966
+ loaded_entries.any? { |resource| resource.dirty? } || @removed.any?
967
+ end
968
+
969
+ # Gets a Human-readable representation of this collection,
970
+ # showing all elements contained in it
971
+ #
972
+ # @return [String]
973
+ # Human-readable representation of this collection, showing all elements
974
+ #
975
+ # @api public
976
+ def inspect
977
+ "[#{map { |resource| resource.inspect }.join(', ')}]"
978
+ end
979
+
980
+ # @api semipublic
981
+ def hash
982
+ self.class.hash ^ query.hash
983
+ end
984
+
985
+ protected
986
+
987
+ # Returns the model key
988
+ #
989
+ # @return [PropertySet]
990
+ # the model key
991
+ #
992
+ # @api private
993
+ def model_key
994
+ model.key(repository_name)
995
+ end
996
+
997
+ # Loaded Resources in the collection
998
+ #
999
+ # @return [Array<Resource>]
1000
+ # Resources in the collection
1001
+ #
1002
+ # @api private
1003
+ def loaded_entries
1004
+ (loaded? ? self : head + tail).reject { |resource| resource.destroyed? }
1005
+ end
1006
+
1007
+ # Returns the PropertySet representing the fields in the Collection scope
1008
+ #
1009
+ # @return [PropertySet]
1010
+ # The set of properties this Collection's query will retrieve
1011
+ #
1012
+ # @api private
1013
+ def properties
1014
+ model.properties(repository_name)
1015
+ end
1016
+
1017
+ # Returns the Relationships for the Collection's Model
1018
+ #
1019
+ # @return [Hash]
1020
+ # The model's relationships, mapping the name to the
1021
+ # Associations::Relationship object
1022
+ #
1023
+ # @api private
1024
+ def relationships
1025
+ model.relationships(repository_name)
1026
+ end
1027
+
1028
+ private
1029
+
1030
+ # Initializes a new Collection identified by the query
1031
+ #
1032
+ # @param [Query] query
1033
+ # Scope the results of the Collection
1034
+ # @param [Enumerable] resources (optional)
1035
+ # List of resources to initialize the Collection with
1036
+ #
1037
+ # @return [self]
1038
+ #
1039
+ # @api private
1040
+ def initialize(query, resources = nil)
1041
+ raise "#{self.class}#new with a block is deprecated" if block_given?
1042
+
1043
+ @query = query
1044
+ @identity_map = IdentityMap.new
1045
+ @removed = Set.new
1046
+
1047
+ super()
1048
+
1049
+ # TODO: change LazyArray to not use a load proc at all
1050
+ remove_instance_variable(:@load_with_proc)
1051
+
1052
+ set(resources) if resources
1053
+ end
1054
+
1055
+ # Copies the original Collection state
1056
+ #
1057
+ # @param [Collection] original
1058
+ # the original collection to copy from
1059
+ #
1060
+ # @return [undefined]
1061
+ #
1062
+ # @api private
1063
+ def initialize_copy(original)
1064
+ super
1065
+ @query = @query.dup
1066
+ @identity_map = @identity_map.dup
1067
+ @removed = @removed.dup
1068
+ end
1069
+
1070
+ # Initialize a resource from a Hash
1071
+ #
1072
+ # @param [Resource, Hash] resource
1073
+ # resource to process
1074
+ #
1075
+ # @return [Resource]
1076
+ # an initialized resource
1077
+ #
1078
+ # @api private
1079
+ def initialize_resource(resource)
1080
+ resource.kind_of?(Hash) ? new(resource) : resource
1081
+ end
1082
+
1083
+ # Test if the collection is loaded between the offset and limit
1084
+ #
1085
+ # @param [Integer] offset
1086
+ # the offset of the collection to test
1087
+ # @param [Integer] limit
1088
+ # optional limit for how many entries to be loaded
1089
+ #
1090
+ # @return [Boolean]
1091
+ # true if the collection is loaded from the offset to the limit
1092
+ #
1093
+ # @api private
1094
+ def partially_loaded?(offset, limit = 1)
1095
+ if offset >= 0
1096
+ lazy_possible?(head, offset + limit)
1097
+ else
1098
+ lazy_possible?(tail, offset.abs)
1099
+ end
1100
+ end
1101
+
1102
+ # Lazy loads a Collection
1103
+ #
1104
+ # @return [self]
1105
+ #
1106
+ # @api private
1107
+ def lazy_load
1108
+ if loaded?
1109
+ return self
1110
+ end
1111
+
1112
+ mark_loaded
1113
+
1114
+ head = self.head
1115
+ tail = self.tail
1116
+ query = self.query
1117
+
1118
+ resources = repository.read(query)
1119
+
1120
+ # remove already known results
1121
+ resources -= head if head.any?
1122
+ resources -= tail if tail.any?
1123
+ resources -= @removed.to_a if @removed.any?
1124
+
1125
+ query.add_reversed? ? unshift(*resources.reverse) : concat(resources)
1126
+
1127
+ # TODO: DRY this up with LazyArray
1128
+ @array.unshift(*head)
1129
+ @array.concat(tail)
1130
+
1131
+ @head = @tail = nil
1132
+ @reapers.each { |resource| @array.delete_if(&resource) } if @reapers
1133
+ @array.freeze if frozen?
1134
+
1135
+ self
1136
+ end
1137
+
1138
+ # Returns the Query Repository name
1139
+ #
1140
+ # @return [Symbol]
1141
+ # the repository name
1142
+ #
1143
+ # @api private
1144
+ def repository_name
1145
+ repository.name
1146
+ end
1147
+
1148
+ # Initializes a new Collection
1149
+ #
1150
+ # @return [Collection]
1151
+ # A new Collection object
1152
+ #
1153
+ # @api private
1154
+ def new_collection(query, resources = nil, &block)
1155
+ if loaded?
1156
+ resources ||= filter(query)
1157
+ end
1158
+
1159
+ # TOOD: figure out a way to pass not-yet-saved Resources to this newly
1160
+ # created Collection. If the new resource matches the conditions, then
1161
+ # it should be added to the collection (keep in mind limit/offset too)
1162
+
1163
+ self.class.new(query, resources, &block)
1164
+ end
1165
+
1166
+ # Apply a set operation on self and another collection
1167
+ #
1168
+ # @param [Symbol] operation
1169
+ # the set operation to apply
1170
+ # @param [Collection] other
1171
+ # the other collection to apply the set operation on
1172
+ #
1173
+ # @return [Collection]
1174
+ # the collection that was created for the set operation
1175
+ #
1176
+ # @api private
1177
+ def set_operation(operation, other)
1178
+ resources = set_operation_resources(operation, other)
1179
+ other_query = Query.target_query(repository, model, other)
1180
+ new_collection(query.send(operation, other_query), resources)
1181
+ end
1182
+
1183
+ # Prepopulate the set operation if the collection is loaded
1184
+ #
1185
+ # @param [Symbol] operation
1186
+ # the set operation to apply
1187
+ # @param [Collection] other
1188
+ # the other collection to apply the set operation on
1189
+ #
1190
+ # @return [nil]
1191
+ # nil if the Collection is not loaded
1192
+ # @return [Array]
1193
+ # the resources to prepopulate the set operation results with
1194
+ #
1195
+ # @api private
1196
+ def set_operation_resources(operation, other)
1197
+ entries.send(operation, other.entries) if loaded?
1198
+ end
1199
+
1200
+ # Creates a resource in the collection
1201
+ #
1202
+ # @param [Boolean] execute_hooks
1203
+ # Whether to execute hooks or not
1204
+ # @param [Hash] attributes
1205
+ # Attributes with which to create the new resource
1206
+ #
1207
+ # @return [Resource]
1208
+ # a saved Resource
1209
+ #
1210
+ # @api private
1211
+ def _create(attributes, execute_hooks = true)
1212
+ resource = repository.scope { model.send(execute_hooks ? :create : :create!, default_attributes.merge(attributes)) }
1213
+ self << resource if resource.saved?
1214
+ resource
1215
+ end
1216
+
1217
+ # Updates a collection
1218
+ #
1219
+ # @return [Boolean]
1220
+ # Returns true if collection was updated
1221
+ #
1222
+ # @api private
1223
+ def _update(dirty_attributes)
1224
+ repository.update(dirty_attributes, self)
1225
+ true
1226
+ end
1227
+
1228
+ # Saves a collection
1229
+ #
1230
+ # @param [Boolean] execute_hooks
1231
+ # Whether to execute hooks or not
1232
+ #
1233
+ # @return [Boolean]
1234
+ # Returns true if collection was updated
1235
+ #
1236
+ # @api private
1237
+ def _save(execute_hooks = true)
1238
+ loaded_entries = self.loaded_entries
1239
+ loaded_entries.each { |resource| set_default_attributes(resource) }
1240
+ @removed.clear
1241
+ loaded_entries.all? { |resource| resource.__send__(execute_hooks ? :save : :save!) }
1242
+ end
1243
+
1244
+ # Returns default values to initialize new Resources in the Collection
1245
+ #
1246
+ # @return [Hash] The default attributes for new instances in this Collection
1247
+ #
1248
+ # @api private
1249
+ def default_attributes
1250
+ return @default_attributes if @default_attributes
1251
+
1252
+ default_attributes = {}
1253
+
1254
+ conditions = query.conditions
1255
+
1256
+ if conditions.slug == :and
1257
+ model_properties = properties.dup
1258
+ model_key = self.model_key
1259
+
1260
+ if model_properties.to_set.superset?(model_key.to_set)
1261
+ model_properties -= model_key
1262
+ end
1263
+
1264
+ conditions.each do |condition|
1265
+ next unless condition.slug == :eql
1266
+
1267
+ subject = condition.subject
1268
+ next unless model_properties.include?(subject) || (condition.relationship? && subject.source_model == model)
1269
+
1270
+ default_attributes[subject] = condition.loaded_value
1271
+ end
1272
+ end
1273
+
1274
+ @default_attributes = default_attributes.freeze
1275
+ end
1276
+
1277
+ # Set the default attributes for a non-frozen resource
1278
+ #
1279
+ # @param [Resource] resource
1280
+ # the resource to set the default attributes for
1281
+ #
1282
+ # @return [undefined]
1283
+ #
1284
+ # @api private
1285
+ def set_default_attributes(resource)
1286
+ unless resource.readonly?
1287
+ resource.attributes = default_attributes
1288
+ end
1289
+ end
1290
+
1291
+ # Track the added resource
1292
+ #
1293
+ # @param [Resource] resource
1294
+ # the resource that was added
1295
+ #
1296
+ # @return [Resource]
1297
+ # the resource that was added
1298
+ #
1299
+ # @api private
1300
+ def resource_added(resource)
1301
+ resource = initialize_resource(resource)
1302
+
1303
+ if resource.saved?
1304
+ @identity_map[resource.key] = resource
1305
+ @removed.delete(resource)
1306
+ else
1307
+ set_default_attributes(resource)
1308
+ end
1309
+
1310
+ resource
1311
+ end
1312
+
1313
+ # Track the added resources
1314
+ #
1315
+ # @param [Array<Resource>] resources
1316
+ # the resources that were added
1317
+ #
1318
+ # @return [Array<Resource>]
1319
+ # the resources that were added
1320
+ #
1321
+ # @api private
1322
+ def resources_added(resources)
1323
+ if resources.kind_of?(Enumerable)
1324
+ resources.map { |resource| resource_added(resource) }
1325
+ else
1326
+ resource_added(resources)
1327
+ end
1328
+ end
1329
+
1330
+ # Track the removed resource
1331
+ #
1332
+ # @param [Resource] resource
1333
+ # the resource that was removed
1334
+ #
1335
+ # @return [Resource]
1336
+ # the resource that was removed
1337
+ #
1338
+ # @api private
1339
+ def resource_removed(resource)
1340
+ if resource.saved?
1341
+ @identity_map.delete(resource.key)
1342
+ @removed << resource
1343
+ end
1344
+
1345
+ resource
1346
+ end
1347
+
1348
+ # Track the removed resources
1349
+ #
1350
+ # @param [Array<Resource>] resources
1351
+ # the resources that were removed
1352
+ #
1353
+ # @return [Array<Resource>]
1354
+ # the resources that were removed
1355
+ #
1356
+ # @api private
1357
+ def resources_removed(resources)
1358
+ if resources.kind_of?(Enumerable)
1359
+ resources.each { |resource| resource_removed(resource) }
1360
+ else
1361
+ resource_removed(resources)
1362
+ end
1363
+ end
1364
+
1365
+ # Filter resources in the collection based on a Query
1366
+ #
1367
+ # @param [Query] query
1368
+ # the query to match each resource in the collection
1369
+ #
1370
+ # @return [Array]
1371
+ # the resources that match the Query
1372
+ # @return [nil]
1373
+ # nil if no resources match the Query
1374
+ #
1375
+ # @api private
1376
+ def filter(other_query)
1377
+ query = self.query
1378
+ fields = query.fields.to_set
1379
+ unique = other_query.unique?
1380
+
1381
+ # TODO: push this into a Query#subset? method
1382
+ if other_query.links.empty? &&
1383
+ (unique || (!unique && !query.unique?)) &&
1384
+ !other_query.reload? &&
1385
+ !other_query.raw? &&
1386
+ other_query.fields.to_set.subset?(fields) &&
1387
+ other_query.condition_properties.subset?(fields)
1388
+ then
1389
+ other_query.filter_records(to_a.dup)
1390
+ end
1391
+ end
1392
+
1393
+ # Return the absolute or relative scoped query
1394
+ #
1395
+ # @param [Query, Hash] query
1396
+ # the query to scope the collection with
1397
+ #
1398
+ # @return [Query]
1399
+ # the absolute or relative scoped query
1400
+ #
1401
+ # @api private
1402
+ def scoped_query(query)
1403
+ if query.kind_of?(Query)
1404
+ query.dup
1405
+ else
1406
+ self.query.relative(query)
1407
+ end
1408
+ end
1409
+
1410
+ # @api private
1411
+ def sliced_query(offset, limit)
1412
+ query = self.query
1413
+
1414
+ if offset >= 0
1415
+ query.slice(offset, limit)
1416
+ else
1417
+ query = query.slice((limit + offset).abs, limit).reverse!
1418
+
1419
+ # tell the Query to prepend each result from the adapter
1420
+ query.update(:add_reversed => !query.add_reversed?)
1421
+ end
1422
+ end
1423
+
1424
+ # Delegates to Model, Relationships or the superclass (LazyArray)
1425
+ #
1426
+ # When this receives a method that belongs to the Model the
1427
+ # Collection is scoped to, it will execute the method within the
1428
+ # same scope as the Collection and return the results.
1429
+ #
1430
+ # When this receives a method that is a relationship the Model has
1431
+ # defined, it will execute the association method within the same
1432
+ # scope as the Collection and return the results.
1433
+ #
1434
+ # Otherwise this method will delegate to a method in the superclass
1435
+ # (LazyArray) and return the results.
1436
+ #
1437
+ # @return [Object]
1438
+ # the return values of the delegated methods
1439
+ #
1440
+ # @api public
1441
+ def method_missing(method, *args, &block)
1442
+ relationships = self.relationships
1443
+
1444
+ if model.respond_to?(method)
1445
+ delegate_to_model(method, *args, &block)
1446
+ elsif relationship = relationships[method] || relationships[DataMapper::Inflector.singularize(method.to_s).to_sym]
1447
+ delegate_to_relationship(relationship, *args)
1448
+ else
1449
+ super
1450
+ end
1451
+ end
1452
+
1453
+ # Delegate the method to the Model
1454
+ #
1455
+ # @param [Symbol] method
1456
+ # the name of the method in the model to execute
1457
+ # @param [Array] *args
1458
+ # the arguments for the method
1459
+ #
1460
+ # @return [Object]
1461
+ # the return value of the model method
1462
+ #
1463
+ # @api private
1464
+ def delegate_to_model(method, *args, &block)
1465
+ model = self.model
1466
+ model.send(:with_scope, query) do
1467
+ model.send(method, *args, &block)
1468
+ end
1469
+ end
1470
+
1471
+ # Delegate the method to the Relationship
1472
+ #
1473
+ # @return [Collection]
1474
+ # the associated Resources
1475
+ #
1476
+ # @api private
1477
+ def delegate_to_relationship(relationship, query = nil)
1478
+ relationship.eager_load(self, query)
1479
+ end
1480
+
1481
+ # Raises an exception if #update is performed on a dirty resource
1482
+ #
1483
+ # @raise [UpdateConflictError]
1484
+ # raise if the resource is dirty
1485
+ #
1486
+ # @return [undefined]
1487
+ #
1488
+ # @api private
1489
+ def assert_update_clean_only(method)
1490
+ if dirty?
1491
+ raise UpdateConflictError, "#{self.class}##{method} cannot be called on a dirty collection"
1492
+ end
1493
+ end
1494
+
1495
+ # Raises an exception if #get receives the wrong number of arguments
1496
+ #
1497
+ # @param [Array] key
1498
+ # the key value
1499
+ #
1500
+ # @return [undefined]
1501
+ #
1502
+ # @raise [UpdateConflictError]
1503
+ # raise if the resource is dirty
1504
+ #
1505
+ # @api private
1506
+ def assert_valid_key_size(key)
1507
+ expected_key_size = model_key.size
1508
+ actual_key_size = key.size
1509
+
1510
+ if actual_key_size != expected_key_size
1511
+ raise ArgumentError, "The number of arguments for the key is invalid, expected #{expected_key_size} but was #{actual_key_size}"
1512
+ end
1513
+ end
1514
+ end # class Collection
1515
+ end # module DataMapper