datamapper-dm-core 0.9.11 → 0.10.0
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 +17 -14
- data/.gitignore +3 -1
- data/FAQ +6 -5
- data/History.txt +5 -39
- data/Manifest.txt +67 -76
- data/QUICKLINKS +1 -1
- data/README.txt +21 -15
- data/Rakefile +16 -15
- data/SPECS +2 -29
- data/TODO +1 -1
- data/dm-core.gemspec +11 -15
- data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
- data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
- data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
- data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
- data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
- data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
- data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
- data/lib/dm-core/adapters.rb +135 -16
- data/lib/dm-core/associations/many_to_many.rb +372 -90
- data/lib/dm-core/associations/many_to_one.rb +220 -73
- data/lib/dm-core/associations/one_to_many.rb +319 -255
- data/lib/dm-core/associations/one_to_one.rb +66 -53
- data/lib/dm-core/associations/relationship.rb +560 -158
- data/lib/dm-core/collection.rb +1104 -381
- data/lib/dm-core/core_ext/kernel.rb +12 -0
- data/lib/dm-core/core_ext/symbol.rb +10 -0
- data/lib/dm-core/identity_map.rb +4 -34
- data/lib/dm-core/migrations.rb +1283 -0
- data/lib/dm-core/model/descendant_set.rb +81 -0
- data/lib/dm-core/model/hook.rb +45 -0
- data/lib/dm-core/model/is.rb +32 -0
- data/lib/dm-core/model/property.rb +248 -0
- data/lib/dm-core/model/relationship.rb +335 -0
- data/lib/dm-core/model/scope.rb +90 -0
- data/lib/dm-core/model.rb +570 -369
- data/lib/dm-core/property.rb +753 -280
- data/lib/dm-core/property_set.rb +141 -98
- data/lib/dm-core/query/conditions/comparison.rb +814 -0
- data/lib/dm-core/query/conditions/operation.rb +247 -0
- data/lib/dm-core/query/direction.rb +43 -0
- data/lib/dm-core/query/operator.rb +42 -0
- data/lib/dm-core/query/path.rb +102 -0
- data/lib/dm-core/query/sort.rb +45 -0
- data/lib/dm-core/query.rb +974 -492
- data/lib/dm-core/repository.rb +147 -107
- data/lib/dm-core/resource.rb +644 -429
- data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
- data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
- data/lib/dm-core/support/chainable.rb +20 -0
- data/lib/dm-core/support/deprecate.rb +12 -0
- data/lib/dm-core/support/equalizer.rb +23 -0
- data/lib/dm-core/support/logger.rb +13 -0
- data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
- data/lib/dm-core/transaction.rb +333 -92
- data/lib/dm-core/type.rb +98 -60
- data/lib/dm-core/types/boolean.rb +1 -1
- data/lib/dm-core/types/discriminator.rb +34 -20
- data/lib/dm-core/types/object.rb +7 -4
- data/lib/dm-core/types/paranoid_boolean.rb +11 -9
- data/lib/dm-core/types/paranoid_datetime.rb +11 -9
- data/lib/dm-core/types/serial.rb +3 -3
- data/lib/dm-core/types/text.rb +3 -4
- data/lib/dm-core/version.rb +1 -1
- data/lib/dm-core.rb +106 -110
- data/script/performance.rb +102 -109
- data/script/profile.rb +169 -38
- data/spec/lib/adapter_helpers.rb +105 -0
- data/spec/lib/collection_helpers.rb +18 -0
- data/spec/lib/counter_adapter.rb +34 -0
- data/spec/lib/pending_helpers.rb +27 -0
- data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
- data/spec/public/associations/many_to_many_spec.rb +193 -0
- data/spec/public/associations/many_to_one_spec.rb +73 -0
- data/spec/public/associations/one_to_many_spec.rb +77 -0
- data/spec/public/associations/one_to_one_spec.rb +156 -0
- data/spec/public/collection_spec.rb +65 -0
- data/spec/public/model/relationship_spec.rb +924 -0
- data/spec/public/model_spec.rb +159 -0
- data/spec/public/property_spec.rb +829 -0
- data/spec/public/resource_spec.rb +71 -0
- data/spec/public/sel_spec.rb +44 -0
- data/spec/public/setup_spec.rb +145 -0
- data/spec/public/shared/association_collection_shared_spec.rb +317 -0
- data/spec/public/shared/collection_shared_spec.rb +1723 -0
- data/spec/public/shared/finder_shared_spec.rb +1619 -0
- data/spec/public/shared/resource_shared_spec.rb +924 -0
- data/spec/public/shared/sel_shared_spec.rb +112 -0
- data/spec/public/transaction_spec.rb +129 -0
- data/spec/public/types/discriminator_spec.rb +130 -0
- data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
- data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
- data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
- data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
- data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
- data/spec/semipublic/associations/relationship_spec.rb +194 -0
- data/spec/semipublic/associations_spec.rb +177 -0
- data/spec/semipublic/collection_spec.rb +142 -0
- data/spec/semipublic/property_spec.rb +61 -0
- data/spec/semipublic/query/conditions_spec.rb +528 -0
- data/spec/semipublic/query/path_spec.rb +443 -0
- data/spec/semipublic/query_spec.rb +2626 -0
- data/spec/semipublic/resource_spec.rb +47 -0
- data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
- data/spec/spec.opts +3 -1
- data/spec/spec_helper.rb +80 -57
- data/tasks/ci.rb +19 -31
- data/tasks/dm.rb +43 -48
- data/tasks/doc.rb +8 -11
- data/tasks/gemspec.rb +5 -5
- data/tasks/hoe.rb +15 -16
- data/tasks/install.rb +8 -10
- metadata +72 -93
- data/lib/dm-core/associations/relationship_chain.rb +0 -81
- data/lib/dm-core/associations.rb +0 -207
- data/lib/dm-core/auto_migrations.rb +0 -105
- data/lib/dm-core/dependency_queue.rb +0 -32
- data/lib/dm-core/hook.rb +0 -11
- data/lib/dm-core/is.rb +0 -16
- data/lib/dm-core/logger.rb +0 -232
- data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
- data/lib/dm-core/migrator.rb +0 -29
- data/lib/dm-core/scope.rb +0 -58
- data/lib/dm-core/support/array.rb +0 -13
- data/lib/dm-core/support/assertions.rb +0 -8
- data/lib/dm-core/support/errors.rb +0 -23
- data/lib/dm-core/support/kernel.rb +0 -11
- data/lib/dm-core/support/symbol.rb +0 -41
- data/lib/dm-core/support.rb +0 -7
- data/lib/dm-core/type_map.rb +0 -80
- data/lib/dm-core/types.rb +0 -19
- data/script/all +0 -4
- data/spec/integration/association_spec.rb +0 -1382
- data/spec/integration/association_through_spec.rb +0 -203
- data/spec/integration/associations/many_to_many_spec.rb +0 -449
- data/spec/integration/associations/many_to_one_spec.rb +0 -163
- data/spec/integration/associations/one_to_many_spec.rb +0 -188
- data/spec/integration/auto_migrations_spec.rb +0 -413
- data/spec/integration/collection_spec.rb +0 -1073
- data/spec/integration/data_objects_adapter_spec.rb +0 -32
- data/spec/integration/dependency_queue_spec.rb +0 -46
- data/spec/integration/model_spec.rb +0 -197
- data/spec/integration/mysql_adapter_spec.rb +0 -85
- data/spec/integration/postgres_adapter_spec.rb +0 -731
- data/spec/integration/property_spec.rb +0 -253
- data/spec/integration/query_spec.rb +0 -514
- data/spec/integration/repository_spec.rb +0 -61
- data/spec/integration/resource_spec.rb +0 -513
- data/spec/integration/sqlite3_adapter_spec.rb +0 -352
- data/spec/integration/sti_spec.rb +0 -273
- data/spec/integration/strategic_eager_loading_spec.rb +0 -156
- data/spec/integration/transaction_spec.rb +0 -75
- data/spec/integration/type_spec.rb +0 -275
- data/spec/lib/logging_helper.rb +0 -18
- data/spec/lib/mock_adapter.rb +0 -27
- data/spec/lib/model_loader.rb +0 -100
- data/spec/lib/publicize_methods.rb +0 -28
- data/spec/models/content.rb +0 -16
- data/spec/models/vehicles.rb +0 -34
- data/spec/models/zoo.rb +0 -48
- data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
- data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
- data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
- data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
- data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
- data/spec/unit/associations/many_to_many_spec.rb +0 -32
- data/spec/unit/associations/many_to_one_spec.rb +0 -159
- data/spec/unit/associations/one_to_many_spec.rb +0 -393
- data/spec/unit/associations/one_to_one_spec.rb +0 -7
- data/spec/unit/associations/relationship_spec.rb +0 -71
- data/spec/unit/associations_spec.rb +0 -242
- data/spec/unit/auto_migrations_spec.rb +0 -111
- data/spec/unit/collection_spec.rb +0 -182
- data/spec/unit/data_mapper_spec.rb +0 -35
- data/spec/unit/identity_map_spec.rb +0 -126
- data/spec/unit/is_spec.rb +0 -80
- data/spec/unit/migrator_spec.rb +0 -33
- data/spec/unit/model_spec.rb +0 -321
- data/spec/unit/naming_conventions_spec.rb +0 -36
- data/spec/unit/property_set_spec.rb +0 -90
- data/spec/unit/property_spec.rb +0 -753
- data/spec/unit/query_spec.rb +0 -571
- data/spec/unit/repository_spec.rb +0 -93
- data/spec/unit/resource_spec.rb +0 -649
- data/spec/unit/scope_spec.rb +0 -142
- data/spec/unit/transaction_spec.rb +0 -493
- data/spec/unit/type_map_spec.rb +0 -114
- data/spec/unit/type_spec.rb +0 -119
|
@@ -1,228 +1,630 @@
|
|
|
1
1
|
module DataMapper
|
|
2
2
|
module Associations
|
|
3
|
+
# Base class for relationships. Each type of relationship
|
|
4
|
+
# (1 to 1, 1 to n, n to m) implements a subclass of this class
|
|
5
|
+
# with methods like get and set overridden.
|
|
3
6
|
class Relationship
|
|
4
|
-
include Assertions
|
|
7
|
+
include Extlib::Assertions
|
|
8
|
+
|
|
9
|
+
OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse ].to_set
|
|
10
|
+
|
|
11
|
+
# Relationship name
|
|
12
|
+
#
|
|
13
|
+
# @example for :parent association in
|
|
14
|
+
#
|
|
15
|
+
# class VersionControl::Commit
|
|
16
|
+
# # ...
|
|
17
|
+
#
|
|
18
|
+
# belongs_to :parent
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# name is :parent
|
|
22
|
+
#
|
|
23
|
+
# @api semipublic
|
|
24
|
+
attr_reader :name
|
|
25
|
+
|
|
26
|
+
# Options used to set up association of this relationship
|
|
27
|
+
#
|
|
28
|
+
# @example for :author association in
|
|
29
|
+
#
|
|
30
|
+
# class VersionControl::Commit
|
|
31
|
+
# # ...
|
|
32
|
+
#
|
|
33
|
+
# belongs_to :author, :model => 'Person'
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# options is a hash with a single key, :model
|
|
37
|
+
#
|
|
38
|
+
# @api semipublic
|
|
39
|
+
attr_reader :options
|
|
40
|
+
|
|
41
|
+
# ivar used to store collection of child options in source
|
|
42
|
+
#
|
|
43
|
+
# @example for :commits association in
|
|
44
|
+
#
|
|
45
|
+
# class VersionControl::Branch
|
|
46
|
+
# # ...
|
|
47
|
+
#
|
|
48
|
+
# has n, :commits
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# instance variable name for source will be @commits
|
|
52
|
+
#
|
|
53
|
+
# @api semipublic
|
|
54
|
+
attr_reader :instance_variable_name
|
|
55
|
+
|
|
56
|
+
# Repository from where child objects are loaded
|
|
57
|
+
#
|
|
58
|
+
# @api semipublic
|
|
59
|
+
attr_reader :child_repository_name
|
|
60
|
+
|
|
61
|
+
# Repository from where parent objects are loaded
|
|
62
|
+
#
|
|
63
|
+
# @api semipublic
|
|
64
|
+
attr_reader :parent_repository_name
|
|
65
|
+
|
|
66
|
+
# Minimum number of child objects for relationship
|
|
67
|
+
#
|
|
68
|
+
# @example for :cores association in
|
|
69
|
+
#
|
|
70
|
+
# class CPU::Multicore
|
|
71
|
+
# # ...
|
|
72
|
+
#
|
|
73
|
+
# has 2..n, :cores
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# minimum is 2
|
|
77
|
+
#
|
|
78
|
+
# @api semipublic
|
|
79
|
+
attr_reader :min
|
|
80
|
+
|
|
81
|
+
# Maximum number of child objects for
|
|
82
|
+
# relationship
|
|
83
|
+
#
|
|
84
|
+
# @example for :fouls association in
|
|
85
|
+
#
|
|
86
|
+
# class Basketball::Player
|
|
87
|
+
# # ...
|
|
88
|
+
#
|
|
89
|
+
# has 0..5, :fouls
|
|
90
|
+
# end
|
|
91
|
+
#
|
|
92
|
+
# maximum is 5
|
|
93
|
+
#
|
|
94
|
+
# @api semipublic
|
|
95
|
+
attr_reader :max
|
|
96
|
+
|
|
97
|
+
# Returns query options for relationship.
|
|
98
|
+
#
|
|
99
|
+
# For this base class, always returns query options
|
|
100
|
+
# has been initialized with.
|
|
101
|
+
# Overriden in subclasses.
|
|
102
|
+
#
|
|
103
|
+
# @api private
|
|
104
|
+
attr_reader :query
|
|
105
|
+
|
|
106
|
+
# Returns a hash of conditions that scopes query that fetches
|
|
107
|
+
# target object
|
|
108
|
+
#
|
|
109
|
+
# @return [Hash]
|
|
110
|
+
# Hash of conditions that scopes query
|
|
111
|
+
#
|
|
112
|
+
# @api private
|
|
113
|
+
def source_scope(source)
|
|
114
|
+
{ inverse => source }
|
|
115
|
+
end
|
|
5
116
|
|
|
6
|
-
|
|
117
|
+
# Creates and returns Query instance that fetches
|
|
118
|
+
# target resource(s) (ex.: articles) for given target resource (ex.: author)
|
|
119
|
+
#
|
|
120
|
+
# @api semipublic
|
|
121
|
+
def query_for(source, other_query = nil)
|
|
122
|
+
repository_name = relative_target_repository_name_for(source)
|
|
123
|
+
|
|
124
|
+
DataMapper.repository(repository_name).scope do
|
|
125
|
+
query = target_model.query.dup
|
|
126
|
+
query.update(self.query)
|
|
127
|
+
query.update(source_scope(source))
|
|
128
|
+
query.update(other_query) if other_query
|
|
129
|
+
query.update(:fields => query.fields | target_key)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
7
132
|
|
|
133
|
+
# Returns model class used by child side of the relationship
|
|
134
|
+
#
|
|
135
|
+
# @return [Resource]
|
|
136
|
+
# Model for association child
|
|
137
|
+
#
|
|
8
138
|
# @api private
|
|
9
|
-
|
|
139
|
+
def child_model
|
|
140
|
+
@child_model ||= (@parent_model || Object).find_const(child_model_name)
|
|
141
|
+
rescue NameError
|
|
142
|
+
raise NameError, "Cannot find the child_model #{child_model_name} for #{parent_model_name} in #{name}"
|
|
143
|
+
end
|
|
10
144
|
|
|
145
|
+
# TODO: document
|
|
11
146
|
# @api private
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
|
|
19
|
-
# TODO: use something similar to DM::NamingConventions to determine the property name
|
|
20
|
-
parent_name = Extlib::Inflection.underscore(Extlib::Inflection.demodulize(parent_model.base_model.name))
|
|
21
|
-
property_name ||= "#{parent_name}_#{parent_property.name}".to_sym
|
|
22
|
-
|
|
23
|
-
if model_properties.has_property?(property_name)
|
|
24
|
-
model_properties[property_name]
|
|
25
|
-
else
|
|
26
|
-
options = {}
|
|
27
|
-
|
|
28
|
-
[ :length, :precision, :scale ].each do |option|
|
|
29
|
-
options[option] = parent_property.send(option)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# NOTE: hack to make each many to many child_key a true key,
|
|
33
|
-
# until I can figure out a better place for this check
|
|
34
|
-
if child_model.respond_to?(:many_to_many)
|
|
35
|
-
options[:key] = true
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
child_model.property(property_name, parent_property.primitive, options)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
PropertySet.new(child_key)
|
|
43
|
-
end
|
|
147
|
+
def child_model?
|
|
148
|
+
child_model
|
|
149
|
+
true
|
|
150
|
+
rescue NameError
|
|
151
|
+
false
|
|
44
152
|
end
|
|
45
153
|
|
|
154
|
+
# TODO: document
|
|
46
155
|
# @api private
|
|
47
|
-
def
|
|
48
|
-
@
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
156
|
+
def child_model_name
|
|
157
|
+
@child_model ? child_model.name : @child_model_name
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns a set of keys that identify the target model
|
|
161
|
+
#
|
|
162
|
+
# @return [PropertySet]
|
|
163
|
+
# a set of properties that identify the target model
|
|
164
|
+
#
|
|
165
|
+
# @api semipublic
|
|
166
|
+
def child_key
|
|
167
|
+
return @child_key if defined?(@child_key)
|
|
168
|
+
|
|
169
|
+
repository_name = child_repository_name || parent_repository_name
|
|
170
|
+
properties = child_model.properties(repository_name)
|
|
171
|
+
|
|
172
|
+
@child_key = if @child_properties
|
|
173
|
+
child_key = properties.values_at(*@child_properties)
|
|
174
|
+
properties.class.new(child_key).freeze
|
|
175
|
+
else
|
|
176
|
+
properties.key
|
|
58
177
|
end
|
|
59
178
|
end
|
|
60
179
|
|
|
180
|
+
# Access Relationship#child_key directly
|
|
181
|
+
#
|
|
182
|
+
# @api private
|
|
183
|
+
alias relationship_child_key child_key
|
|
184
|
+
private :relationship_child_key
|
|
185
|
+
|
|
186
|
+
# Returns model class used by parent side of the relationship
|
|
187
|
+
#
|
|
188
|
+
# @return [Resource]
|
|
189
|
+
# Class of association parent
|
|
190
|
+
#
|
|
61
191
|
# @api private
|
|
62
192
|
def parent_model
|
|
63
|
-
|
|
64
|
-
@parent_model = @child_model.find_const(@parent_model)
|
|
193
|
+
@parent_model ||= (@child_model || Object).find_const(parent_model_name)
|
|
65
194
|
rescue NameError
|
|
66
|
-
raise NameError, "Cannot find the parent_model #{
|
|
195
|
+
raise NameError, "Cannot find the parent_model #{parent_model_name} for #{child_model_name} in #{name}"
|
|
67
196
|
end
|
|
68
197
|
|
|
198
|
+
# TODO: document
|
|
69
199
|
# @api private
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
200
|
+
def parent_model?
|
|
201
|
+
parent_model
|
|
202
|
+
true
|
|
73
203
|
rescue NameError
|
|
74
|
-
|
|
204
|
+
false
|
|
75
205
|
end
|
|
76
206
|
|
|
207
|
+
# TODO: document
|
|
77
208
|
# @api private
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
209
|
+
def parent_model_name
|
|
210
|
+
@parent_model ? parent_model.name : @parent_model_name
|
|
211
|
+
end
|
|
81
212
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
213
|
+
# Returns a set of keys that identify parent model
|
|
214
|
+
#
|
|
215
|
+
# @return [PropertySet]
|
|
216
|
+
# a set of properties that identify parent model
|
|
217
|
+
#
|
|
218
|
+
# @api private
|
|
219
|
+
def parent_key
|
|
220
|
+
return @parent_key if defined?(@parent_key)
|
|
85
221
|
|
|
86
|
-
|
|
87
|
-
|
|
222
|
+
repository_name = parent_repository_name || child_repository_name
|
|
223
|
+
properties = parent_model.properties(repository_name)
|
|
88
224
|
|
|
89
|
-
|
|
90
|
-
|
|
225
|
+
@parent_key = if @parent_properties
|
|
226
|
+
parent_key = properties.values_at(*@parent_properties)
|
|
227
|
+
properties.class.new(parent_key).freeze
|
|
228
|
+
else
|
|
229
|
+
properties.key
|
|
230
|
+
end
|
|
231
|
+
end
|
|
91
232
|
|
|
92
|
-
|
|
233
|
+
# Loads and returns "other end" of the association.
|
|
234
|
+
# Must be implemented in subclasses.
|
|
235
|
+
#
|
|
236
|
+
# @api semipublic
|
|
237
|
+
def get(resource, other_query = nil)
|
|
238
|
+
raise NotImplementedError, "#{self.class}#get not implemented"
|
|
239
|
+
end
|
|
93
240
|
|
|
94
|
-
|
|
241
|
+
# Gets "other end" of the association directly
|
|
242
|
+
# as @ivar on given resource. Subclasses usually
|
|
243
|
+
# use implementation of this class.
|
|
244
|
+
#
|
|
245
|
+
# @api semipublic
|
|
246
|
+
def get!(resource)
|
|
247
|
+
resource.instance_variable_get(instance_variable_name)
|
|
248
|
+
end
|
|
95
249
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
250
|
+
# Sets value of the "other end" of association
|
|
251
|
+
# on given resource. Must be implemented in subclasses.
|
|
252
|
+
#
|
|
253
|
+
# @api semipublic
|
|
254
|
+
def set(resource, association)
|
|
255
|
+
raise NotImplementedError, "#{self.class}#set not implemented"
|
|
256
|
+
end
|
|
103
257
|
|
|
104
|
-
|
|
258
|
+
# Sets "other end" of the association directly
|
|
259
|
+
# as @ivar on given resource. Subclasses usually
|
|
260
|
+
# use implementation of this class.
|
|
261
|
+
#
|
|
262
|
+
# @api semipublic
|
|
263
|
+
def set!(resource, association)
|
|
264
|
+
resource.instance_variable_set(instance_variable_name, association)
|
|
265
|
+
end
|
|
105
266
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
267
|
+
# Eager load the collection using the source as a base
|
|
268
|
+
#
|
|
269
|
+
# @param [Resource, Collection] source
|
|
270
|
+
# the source to query with
|
|
271
|
+
# @param [Query, Hash] query
|
|
272
|
+
# optional query to restrict the collection
|
|
273
|
+
#
|
|
274
|
+
# @return [Collection]
|
|
275
|
+
# the loaded collection for the source
|
|
276
|
+
#
|
|
277
|
+
# @api private
|
|
278
|
+
def eager_load(source, query = nil)
|
|
279
|
+
target_maps = Hash.new { |h,k| h[k] = [] }
|
|
109
280
|
|
|
110
|
-
|
|
111
|
-
query.conditions.map! do |operator, property, bind_value|
|
|
112
|
-
if operator != :raw && child_key.has_property?(property.name)
|
|
113
|
-
bind_value = *children.map { |child| property.get(child) }.uniq
|
|
114
|
-
end
|
|
115
|
-
[ operator, property, bind_value ]
|
|
116
|
-
end
|
|
281
|
+
collection_query = query_for(source, query)
|
|
117
282
|
|
|
118
|
-
|
|
119
|
-
|
|
283
|
+
# TODO: create an object that wraps this logic, and when the first
|
|
284
|
+
# kicker is fired, then it'll load up the collection, and then
|
|
285
|
+
# populate all the other methods
|
|
120
286
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
association.instance_variable_set(:@children, parents_children)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
287
|
+
collection = source.model.all(collection_query).each do |target|
|
|
288
|
+
target_maps[target_key.get(target)] << target
|
|
289
|
+
end
|
|
127
290
|
|
|
128
|
-
|
|
291
|
+
Array(source).each do |source|
|
|
292
|
+
key = target_key.typecast(source_key.get(source))
|
|
293
|
+
eager_load_targets(source, target_maps[key], query)
|
|
129
294
|
end
|
|
295
|
+
|
|
296
|
+
collection
|
|
130
297
|
end
|
|
131
298
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
299
|
+
# Checks if "other end" of association is loaded on given
|
|
300
|
+
# resource.
|
|
301
|
+
#
|
|
302
|
+
# @api semipublic
|
|
303
|
+
def loaded?(resource)
|
|
304
|
+
assert_kind_of 'resource', resource, source_model
|
|
136
305
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
child_identity_map = child.repository.identity_map(child_model.base_model)
|
|
306
|
+
resource.instance_variable_defined?(instance_variable_name)
|
|
307
|
+
end
|
|
140
308
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
309
|
+
# Test the source to see if it is a valid target
|
|
310
|
+
#
|
|
311
|
+
# @param [Object] source
|
|
312
|
+
# the resource or collection to be tested
|
|
313
|
+
#
|
|
314
|
+
# @return [Boolean]
|
|
315
|
+
# true if the resource is valid
|
|
316
|
+
#
|
|
317
|
+
# @api semipulic
|
|
318
|
+
def valid?(source)
|
|
319
|
+
return true if source.nil?
|
|
320
|
+
|
|
321
|
+
case source
|
|
322
|
+
when Array, Collection then valid_collection?(source)
|
|
323
|
+
when Resource then valid_resource?(source)
|
|
324
|
+
else
|
|
325
|
+
raise ArgumentError, "+source+ should be an Array or Resource, but was a #{source.class.name}"
|
|
326
|
+
end
|
|
327
|
+
end
|
|
144
328
|
|
|
145
|
-
|
|
146
|
-
|
|
329
|
+
# Compares another Relationship for equality
|
|
330
|
+
#
|
|
331
|
+
# @param [Relationship] other
|
|
332
|
+
# the other Relationship to compare with
|
|
333
|
+
#
|
|
334
|
+
# @return [Boolean]
|
|
335
|
+
# true if they are equal, false if not
|
|
336
|
+
#
|
|
337
|
+
# @api public
|
|
338
|
+
def eql?(other)
|
|
339
|
+
return true if equal?(other)
|
|
340
|
+
instance_of?(other.class) && cmp?(other, :eql?)
|
|
341
|
+
end
|
|
147
342
|
|
|
148
|
-
|
|
149
|
-
|
|
343
|
+
# Compares another Relationship for equivalency
|
|
344
|
+
#
|
|
345
|
+
# @param [Relationship] other
|
|
346
|
+
# the other Relationship to compare with
|
|
347
|
+
#
|
|
348
|
+
# @return [Boolean]
|
|
349
|
+
# true if they are equal, false if not
|
|
350
|
+
#
|
|
351
|
+
# @api public
|
|
352
|
+
def ==(other)
|
|
353
|
+
return true if equal?(other)
|
|
354
|
+
return false if kind_of_inverse?(other)
|
|
355
|
+
other.respond_to?(:cmp_repository?, true) &&
|
|
356
|
+
other.respond_to?(:cmp_model?, true) &&
|
|
357
|
+
other.respond_to?(:cmp_key?, true) &&
|
|
358
|
+
other.respond_to?(:query) &&
|
|
359
|
+
cmp?(other, :==)
|
|
360
|
+
end
|
|
150
361
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
362
|
+
# Get the inverse relationship from the target model
|
|
363
|
+
#
|
|
364
|
+
# @api semipublic
|
|
365
|
+
def inverse
|
|
366
|
+
return @inverse if defined?(@inverse)
|
|
154
367
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
collection.send(:lazy_load)
|
|
158
|
-
children.each do |c|
|
|
159
|
-
c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
|
|
160
|
-
end
|
|
161
|
-
child.send(association_accessor).instance_variable_get(:@parent)
|
|
162
|
-
end
|
|
368
|
+
if kind_of_inverse?(options[:inverse])
|
|
369
|
+
return @inverse = options[:inverse]
|
|
163
370
|
end
|
|
371
|
+
|
|
372
|
+
relationships = target_model.relationships(relative_target_repository_name).values
|
|
373
|
+
|
|
374
|
+
@inverse = relationships.detect { |relationship| inverse?(relationship) } ||
|
|
375
|
+
invert
|
|
376
|
+
|
|
377
|
+
@inverse.child_key
|
|
378
|
+
|
|
379
|
+
@inverse
|
|
164
380
|
end
|
|
165
381
|
|
|
382
|
+
# TODO: document
|
|
166
383
|
# @api private
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
384
|
+
def relative_target_repository_name
|
|
385
|
+
target_repository_name || source_repository_name
|
|
386
|
+
end
|
|
170
387
|
|
|
171
|
-
|
|
172
|
-
|
|
388
|
+
# TODO: document
|
|
389
|
+
# @api private
|
|
390
|
+
def relative_target_repository_name_for(source)
|
|
391
|
+
target_repository_name || if source.respond_to?(:repository)
|
|
392
|
+
source.repository.name
|
|
173
393
|
else
|
|
174
|
-
|
|
394
|
+
source_repository_name
|
|
175
395
|
end
|
|
176
396
|
end
|
|
177
397
|
|
|
398
|
+
private
|
|
399
|
+
|
|
400
|
+
# TODO: document
|
|
178
401
|
# @api private
|
|
179
|
-
|
|
180
|
-
child_key.set(child, parent && parent_key.get(parent))
|
|
181
|
-
end
|
|
402
|
+
attr_reader :child_properties
|
|
182
403
|
|
|
183
|
-
|
|
404
|
+
# TODO: document
|
|
405
|
+
# @api private
|
|
406
|
+
attr_reader :parent_properties
|
|
407
|
+
|
|
408
|
+
# Initializes new Relationship: sets attributes of relationship
|
|
409
|
+
# from options as well as conventions: for instance, @ivar name
|
|
410
|
+
# for association is constructed by prefixing @ to association name.
|
|
411
|
+
#
|
|
412
|
+
# Once attributes are set, reader and writer are created for
|
|
413
|
+
# the resource association belongs to
|
|
414
|
+
#
|
|
415
|
+
# @api semipublic
|
|
416
|
+
def initialize(name, child_model, parent_model, options = {})
|
|
417
|
+
initialize_object_ivar('child_model', child_model)
|
|
418
|
+
initialize_object_ivar('parent_model', parent_model)
|
|
419
|
+
|
|
420
|
+
@name = name
|
|
421
|
+
@instance_variable_name = "@#{@name}".freeze
|
|
422
|
+
@options = options.dup.freeze
|
|
423
|
+
@child_repository_name = @options[:child_repository_name]
|
|
424
|
+
@parent_repository_name = @options[:parent_repository_name]
|
|
425
|
+
@child_properties = @options[:child_key].try_dup.freeze
|
|
426
|
+
@parent_properties = @options[:parent_key].try_dup.freeze
|
|
427
|
+
@min = @options[:min]
|
|
428
|
+
@max = @options[:max]
|
|
429
|
+
|
|
430
|
+
# TODO: normalize the @query to become :conditions => AndOperation
|
|
431
|
+
# - Property/Relationship/Path should be left alone
|
|
432
|
+
# - Symbol/String keys should become a Property, scoped to the target_repository and target_model
|
|
433
|
+
# - Extract subject (target) from Operator
|
|
434
|
+
# - subject should be processed same as above
|
|
435
|
+
# - each subject should be transformed into AbstractComparison
|
|
436
|
+
# object with the subject, operator and value
|
|
437
|
+
# - transform into an AndOperation object, and return the
|
|
438
|
+
# query as :condition => and_object from self.query
|
|
439
|
+
# - this should provide the best performance
|
|
440
|
+
|
|
441
|
+
@query = @options.except(*self.class::OPTIONS).freeze
|
|
442
|
+
|
|
443
|
+
create_reader
|
|
444
|
+
create_writer
|
|
445
|
+
end
|
|
184
446
|
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
447
|
+
# Set the correct ivars for the named object
|
|
448
|
+
#
|
|
449
|
+
# This method should set the object in an ivar with the same name
|
|
450
|
+
# provided, plus it should set a String form of the object in
|
|
451
|
+
# a second ivar.
|
|
452
|
+
#
|
|
453
|
+
# @param [String]
|
|
454
|
+
# the name of the ivar to set
|
|
455
|
+
# @param [#name, #to_str, #to_sym] object
|
|
456
|
+
# the object to set in the ivar
|
|
457
|
+
#
|
|
458
|
+
# @return [String]
|
|
459
|
+
# the String value
|
|
460
|
+
#
|
|
461
|
+
# @raise [ArgumentError]
|
|
462
|
+
# raise when object does not respond to expected methods
|
|
463
|
+
#
|
|
464
|
+
# @api private
|
|
465
|
+
def initialize_object_ivar(name, object)
|
|
466
|
+
if object.respond_to?(:name)
|
|
467
|
+
instance_variable_set("@#{name}", object)
|
|
468
|
+
initialize_object_ivar(name, object.name)
|
|
469
|
+
elsif object.respond_to?(:to_str)
|
|
470
|
+
instance_variable_set("@#{name}_name", object.to_str.dup.freeze)
|
|
471
|
+
elsif object.respond_to?(:to_sym)
|
|
472
|
+
instance_variable_set("@#{name}_name", object.to_sym)
|
|
473
|
+
else
|
|
474
|
+
raise ArgumentError, "#{name} does not respond to #to_str or #name"
|
|
197
475
|
end
|
|
198
476
|
|
|
199
|
-
|
|
200
|
-
|
|
477
|
+
object
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Creates reader method for association.
|
|
481
|
+
#
|
|
482
|
+
# Must be implemented by subclasses.
|
|
483
|
+
#
|
|
484
|
+
# @api semipublic
|
|
485
|
+
def create_reader
|
|
486
|
+
raise NotImplementedError, "#{self.class}#create_reader not implemented"
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Creates both writer method for association.
|
|
490
|
+
#
|
|
491
|
+
# Must be implemented by subclasses.
|
|
492
|
+
#
|
|
493
|
+
# @api semipublic
|
|
494
|
+
def create_writer
|
|
495
|
+
raise NotImplementedError, "#{self.class}#create_writer not implemented"
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Sets the association targets in the resource
|
|
499
|
+
#
|
|
500
|
+
# @param [Resource] source
|
|
501
|
+
# the source to set
|
|
502
|
+
# @param [Array<Resource>] targets
|
|
503
|
+
# the targets for the association
|
|
504
|
+
# @param [Query, Hash] query
|
|
505
|
+
# the query to scope the association with
|
|
506
|
+
#
|
|
507
|
+
# @return [undefined]
|
|
508
|
+
#
|
|
509
|
+
# @api private
|
|
510
|
+
def eager_load_targets(source, targets, query)
|
|
511
|
+
raise NotImplementedError, "#{self.class}#eager_load_targets not implemented"
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# TODO: document
|
|
515
|
+
# @api private
|
|
516
|
+
def valid_collection?(collection)
|
|
517
|
+
if collection.instance_of?(Array) || collection.loaded?
|
|
518
|
+
collection.all? { |resource| valid_resource?(resource) }
|
|
519
|
+
else
|
|
520
|
+
collection.model <= target_model && (collection.query.fields & target_key) == target_key
|
|
201
521
|
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# TODO: document
|
|
525
|
+
# @api private
|
|
526
|
+
def valid_resource?(resource)
|
|
527
|
+
resource.kind_of?(target_model) &&
|
|
528
|
+
target_key.zip(target_key.get!(resource)).all? { |property, value| property.valid?(value) }
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# TODO: document
|
|
532
|
+
# @api private
|
|
533
|
+
def inverse?(other)
|
|
534
|
+
return true if @inverse.equal?(other)
|
|
535
|
+
|
|
536
|
+
other != self &&
|
|
537
|
+
kind_of_inverse?(other) &&
|
|
538
|
+
cmp_repository?(other, :==, :child) &&
|
|
539
|
+
cmp_repository?(other, :==, :parent) &&
|
|
540
|
+
cmp_model?(other, :==, :child) &&
|
|
541
|
+
cmp_model?(other, :==, :parent) &&
|
|
542
|
+
cmp_key?(other, :==, :child) &&
|
|
543
|
+
cmp_key?(other, :==, :parent)
|
|
544
|
+
|
|
545
|
+
# TODO: match only when the Query is empty, or is the same as the
|
|
546
|
+
# default scope for the target model
|
|
547
|
+
end
|
|
202
548
|
|
|
203
|
-
|
|
204
|
-
|
|
549
|
+
# TODO: document
|
|
550
|
+
# @api private
|
|
551
|
+
def inverse_name
|
|
552
|
+
if options[:inverse].kind_of?(Relationship)
|
|
553
|
+
options[:inverse].name
|
|
554
|
+
else
|
|
555
|
+
options[:inverse]
|
|
205
556
|
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# TODO: document
|
|
560
|
+
# @api private
|
|
561
|
+
def invert
|
|
562
|
+
inverse_class.new(inverse_name, child_model, parent_model, inverted_options)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# TODO: document
|
|
566
|
+
# @api private
|
|
567
|
+
def inverted_options
|
|
568
|
+
options.only(*OPTIONS - [ :min, :max ]).update(:inverse => self)
|
|
569
|
+
end
|
|
206
570
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@options = options
|
|
215
|
-
|
|
216
|
-
# attempt to load the child_key if the parent and child model constants are defined
|
|
217
|
-
if model_defined?(@child_model) && model_defined?(@parent_model)
|
|
218
|
-
child_key
|
|
571
|
+
# TODO: document
|
|
572
|
+
# @api private
|
|
573
|
+
def options_with_inverse
|
|
574
|
+
if child_model? && parent_model?
|
|
575
|
+
options.merge(:inverse => inverse)
|
|
576
|
+
else
|
|
577
|
+
options.merge(:inverse => inverse_name)
|
|
219
578
|
end
|
|
220
579
|
end
|
|
221
580
|
|
|
581
|
+
# TODO: document
|
|
582
|
+
# @api private
|
|
583
|
+
def kind_of_inverse?(other)
|
|
584
|
+
other.kind_of?(inverse_class)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# TODO: document
|
|
222
588
|
# @api private
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
589
|
+
def cmp?(other, operator)
|
|
590
|
+
name.send(operator, other.name) &&
|
|
591
|
+
cmp_repository?(other, operator, :child) &&
|
|
592
|
+
cmp_repository?(other, operator, :parent) &&
|
|
593
|
+
cmp_model?(other, operator, :child) &&
|
|
594
|
+
cmp_model?(other, operator, :parent) &&
|
|
595
|
+
cmp_key?(other, operator, :child) &&
|
|
596
|
+
cmp_key?(other, operator, :parent) &&
|
|
597
|
+
query.send(operator, other.query)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# TODO: document
|
|
601
|
+
# @api private
|
|
602
|
+
def cmp_repository?(other, operator, type)
|
|
603
|
+
# if either repository is nil, then the relationship is relative,
|
|
604
|
+
# and the repositories are considered equivalent
|
|
605
|
+
return true unless repository_name = send("#{type}_repository_name")
|
|
606
|
+
return true unless other_repository_name = other.send("#{type}_repository_name")
|
|
607
|
+
|
|
608
|
+
repository_name.send(operator, other_repository_name)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# TODO: document
|
|
612
|
+
# @api private
|
|
613
|
+
def cmp_model?(other, operator, type)
|
|
614
|
+
send("#{type}_model?") &&
|
|
615
|
+
other.send("#{type}_model?") &&
|
|
616
|
+
send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# TODO: document
|
|
620
|
+
# @api private
|
|
621
|
+
def cmp_key?(other, operator, type)
|
|
622
|
+
property_method = "#{type}_properties"
|
|
623
|
+
|
|
624
|
+
self_key = send(property_method)
|
|
625
|
+
other_key = other.send(property_method)
|
|
626
|
+
|
|
627
|
+
self_key.send(operator, other_key)
|
|
226
628
|
end
|
|
227
629
|
end # class Relationship
|
|
228
630
|
end # module Associations
|