ardm-core 1.2.1

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