ghost_dm-core 1.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (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