ghost_dm-core 1.3.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +29 -0
- data/.document +5 -0
- data/.gitignore +35 -0
- data/.yardopts +1 -0
- data/Gemfile +65 -0
- data/LICENSE +20 -0
- data/README.md +269 -0
- data/Rakefile +4 -0
- data/dm-core.gemspec +24 -0
- data/lib/dm-core.rb +292 -0
- data/lib/dm-core/adapters.rb +222 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +237 -0
- data/lib/dm-core/adapters/in_memory_adapter.rb +113 -0
- data/lib/dm-core/associations/many_to_many.rb +499 -0
- data/lib/dm-core/associations/many_to_one.rb +290 -0
- data/lib/dm-core/associations/one_to_many.rb +348 -0
- data/lib/dm-core/associations/one_to_one.rb +86 -0
- data/lib/dm-core/associations/relationship.rb +663 -0
- data/lib/dm-core/backwards.rb +13 -0
- data/lib/dm-core/collection.rb +1515 -0
- data/lib/dm-core/core_ext/kernel.rb +23 -0
- data/lib/dm-core/core_ext/pathname.rb +6 -0
- data/lib/dm-core/core_ext/symbol.rb +10 -0
- data/lib/dm-core/identity_map.rb +7 -0
- data/lib/dm-core/model.rb +874 -0
- data/lib/dm-core/model/hook.rb +103 -0
- data/lib/dm-core/model/is.rb +32 -0
- data/lib/dm-core/model/property.rb +249 -0
- data/lib/dm-core/model/relationship.rb +378 -0
- data/lib/dm-core/model/scope.rb +89 -0
- data/lib/dm-core/property.rb +866 -0
- data/lib/dm-core/property/binary.rb +21 -0
- data/lib/dm-core/property/boolean.rb +20 -0
- data/lib/dm-core/property/class.rb +17 -0
- data/lib/dm-core/property/date.rb +10 -0
- data/lib/dm-core/property/date_time.rb +10 -0
- data/lib/dm-core/property/decimal.rb +36 -0
- data/lib/dm-core/property/discriminator.rb +44 -0
- data/lib/dm-core/property/float.rb +16 -0
- data/lib/dm-core/property/integer.rb +22 -0
- data/lib/dm-core/property/invalid_value_error.rb +22 -0
- data/lib/dm-core/property/lookup.rb +27 -0
- data/lib/dm-core/property/numeric.rb +38 -0
- data/lib/dm-core/property/object.rb +34 -0
- data/lib/dm-core/property/serial.rb +14 -0
- data/lib/dm-core/property/string.rb +38 -0
- data/lib/dm-core/property/text.rb +9 -0
- data/lib/dm-core/property/time.rb +10 -0
- data/lib/dm-core/property_set.rb +177 -0
- data/lib/dm-core/query.rb +1366 -0
- data/lib/dm-core/query/conditions/comparison.rb +911 -0
- data/lib/dm-core/query/conditions/operation.rb +721 -0
- data/lib/dm-core/query/direction.rb +36 -0
- data/lib/dm-core/query/operator.rb +35 -0
- data/lib/dm-core/query/path.rb +114 -0
- data/lib/dm-core/query/sort.rb +39 -0
- data/lib/dm-core/relationship_set.rb +72 -0
- data/lib/dm-core/repository.rb +226 -0
- data/lib/dm-core/resource.rb +1214 -0
- data/lib/dm-core/resource/persistence_state.rb +75 -0
- data/lib/dm-core/resource/persistence_state/clean.rb +40 -0
- data/lib/dm-core/resource/persistence_state/deleted.rb +30 -0
- data/lib/dm-core/resource/persistence_state/dirty.rb +96 -0
- data/lib/dm-core/resource/persistence_state/immutable.rb +34 -0
- data/lib/dm-core/resource/persistence_state/persisted.rb +29 -0
- data/lib/dm-core/resource/persistence_state/transient.rb +80 -0
- data/lib/dm-core/spec/lib/adapter_helpers.rb +64 -0
- data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
- data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
- data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
- data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
- data/lib/dm-core/spec/setup.rb +174 -0
- data/lib/dm-core/spec/shared/adapter_spec.rb +341 -0
- data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
- data/lib/dm-core/spec/shared/resource_spec.rb +1232 -0
- data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
- data/lib/dm-core/spec/shared/semipublic/property_spec.rb +176 -0
- data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
- data/lib/dm-core/support/assertions.rb +8 -0
- data/lib/dm-core/support/chainable.rb +18 -0
- data/lib/dm-core/support/deprecate.rb +12 -0
- data/lib/dm-core/support/descendant_set.rb +89 -0
- data/lib/dm-core/support/equalizer.rb +48 -0
- data/lib/dm-core/support/ext/array.rb +22 -0
- data/lib/dm-core/support/ext/blank.rb +25 -0
- data/lib/dm-core/support/ext/hash.rb +67 -0
- data/lib/dm-core/support/ext/module.rb +47 -0
- data/lib/dm-core/support/ext/object.rb +57 -0
- data/lib/dm-core/support/ext/string.rb +24 -0
- data/lib/dm-core/support/ext/try_dup.rb +12 -0
- data/lib/dm-core/support/hook.rb +405 -0
- data/lib/dm-core/support/inflections.rb +60 -0
- data/lib/dm-core/support/inflector/inflections.rb +211 -0
- data/lib/dm-core/support/inflector/methods.rb +151 -0
- data/lib/dm-core/support/lazy_array.rb +451 -0
- data/lib/dm-core/support/local_object_space.rb +13 -0
- data/lib/dm-core/support/logger.rb +201 -0
- data/lib/dm-core/support/mash.rb +176 -0
- data/lib/dm-core/support/naming_conventions.rb +90 -0
- data/lib/dm-core/support/ordered_set.rb +380 -0
- data/lib/dm-core/support/subject.rb +33 -0
- data/lib/dm-core/support/subject_set.rb +250 -0
- data/lib/dm-core/version.rb +3 -0
- data/script/performance.rb +275 -0
- data/script/profile.rb +218 -0
- data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
- data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +68 -0
- data/spec/public/associations/many_to_many_spec.rb +197 -0
- data/spec/public/associations/many_to_one_spec.rb +83 -0
- data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
- data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
- data/spec/public/associations/one_to_many_spec.rb +81 -0
- data/spec/public/associations/one_to_one_spec.rb +176 -0
- data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
- data/spec/public/collection_spec.rb +69 -0
- data/spec/public/finalize_spec.rb +76 -0
- data/spec/public/model/hook_spec.rb +246 -0
- data/spec/public/model/property_spec.rb +88 -0
- data/spec/public/model/relationship_spec.rb +1040 -0
- data/spec/public/model_spec.rb +462 -0
- data/spec/public/property/binary_spec.rb +41 -0
- data/spec/public/property/boolean_spec.rb +22 -0
- data/spec/public/property/class_spec.rb +28 -0
- data/spec/public/property/date_spec.rb +22 -0
- data/spec/public/property/date_time_spec.rb +22 -0
- data/spec/public/property/decimal_spec.rb +23 -0
- data/spec/public/property/discriminator_spec.rb +135 -0
- data/spec/public/property/float_spec.rb +22 -0
- data/spec/public/property/integer_spec.rb +22 -0
- data/spec/public/property/object_spec.rb +107 -0
- data/spec/public/property/serial_spec.rb +22 -0
- data/spec/public/property/string_spec.rb +22 -0
- data/spec/public/property/text_spec.rb +63 -0
- data/spec/public/property/time_spec.rb +22 -0
- data/spec/public/property_spec.rb +341 -0
- data/spec/public/resource_spec.rb +288 -0
- data/spec/public/sel_spec.rb +53 -0
- data/spec/public/setup_spec.rb +145 -0
- data/spec/public/shared/association_collection_shared_spec.rb +309 -0
- data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
- data/spec/public/shared/collection_shared_spec.rb +1667 -0
- data/spec/public/shared/finder_shared_spec.rb +1629 -0
- data/spec/rcov.opts +6 -0
- data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
- data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
- data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
- data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
- data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
- data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
- data/spec/semipublic/associations/relationship_spec.rb +200 -0
- data/spec/semipublic/associations_spec.rb +177 -0
- data/spec/semipublic/collection_spec.rb +110 -0
- data/spec/semipublic/model_spec.rb +96 -0
- data/spec/semipublic/property/binary_spec.rb +13 -0
- data/spec/semipublic/property/boolean_spec.rb +47 -0
- data/spec/semipublic/property/class_spec.rb +33 -0
- data/spec/semipublic/property/date_spec.rb +43 -0
- data/spec/semipublic/property/date_time_spec.rb +46 -0
- data/spec/semipublic/property/decimal_spec.rb +83 -0
- data/spec/semipublic/property/discriminator_spec.rb +19 -0
- data/spec/semipublic/property/float_spec.rb +82 -0
- data/spec/semipublic/property/integer_spec.rb +82 -0
- data/spec/semipublic/property/lookup_spec.rb +29 -0
- data/spec/semipublic/property/serial_spec.rb +13 -0
- data/spec/semipublic/property/string_spec.rb +13 -0
- data/spec/semipublic/property/text_spec.rb +31 -0
- data/spec/semipublic/property/time_spec.rb +50 -0
- data/spec/semipublic/property_spec.rb +114 -0
- data/spec/semipublic/query/conditions/comparison_spec.rb +1501 -0
- data/spec/semipublic/query/conditions/operation_spec.rb +1294 -0
- data/spec/semipublic/query/path_spec.rb +471 -0
- data/spec/semipublic/query_spec.rb +3682 -0
- data/spec/semipublic/resource/state/clean_spec.rb +88 -0
- data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
- data/spec/semipublic/resource/state/dirty_spec.rb +162 -0
- data/spec/semipublic/resource/state/immutable_spec.rb +105 -0
- data/spec/semipublic/resource/state/transient_spec.rb +162 -0
- data/spec/semipublic/resource/state_spec.rb +230 -0
- data/spec/semipublic/resource_spec.rb +23 -0
- data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
- data/spec/semipublic/shared/resource_shared_spec.rb +199 -0
- data/spec/semipublic/shared/resource_state_shared_spec.rb +79 -0
- data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/core_ext/hash.rb +10 -0
- data/spec/support/core_ext/inheritable_attributes.rb +46 -0
- data/spec/support/properties/huge_integer.rb +17 -0
- data/spec/unit/array_spec.rb +23 -0
- data/spec/unit/blank_spec.rb +73 -0
- data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
- data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
- data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
- data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
- data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
- data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
- data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
- data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
- data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
- data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
- data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
- data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
- data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
- data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
- data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
- data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
- data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
- data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
- data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
- data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
- data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
- data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
- data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
- data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
- data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
- data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
- data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
- data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
- data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
- data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
- data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
- data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
- data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
- data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
- data/spec/unit/hash_spec.rb +28 -0
- data/spec/unit/hook_spec.rb +1235 -0
- data/spec/unit/inflections_spec.rb +16 -0
- data/spec/unit/lazy_array_spec.rb +1949 -0
- data/spec/unit/mash_spec.rb +312 -0
- data/spec/unit/module_spec.rb +71 -0
- data/spec/unit/object_spec.rb +38 -0
- data/spec/unit/try_dup_spec.rb +46 -0
- data/tasks/ci.rake +1 -0
- data/tasks/spec.rake +38 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- 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
|