dm-core 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (194) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -50
  5. data/Manifest.txt +66 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +6 -7
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/deps.rip +2 -0
  12. data/dm-core.gemspec +11 -15
  13. data/lib/dm-core.rb +105 -110
  14. data/lib/dm-core/adapters.rb +135 -16
  15. data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
  16. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  17. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  18. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  19. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  22. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  23. data/lib/dm-core/associations/many_to_many.rb +372 -90
  24. data/lib/dm-core/associations/many_to_one.rb +220 -73
  25. data/lib/dm-core/associations/one_to_many.rb +319 -255
  26. data/lib/dm-core/associations/one_to_one.rb +66 -53
  27. data/lib/dm-core/associations/relationship.rb +561 -156
  28. data/lib/dm-core/collection.rb +1101 -379
  29. data/lib/dm-core/core_ext/kernel.rb +12 -0
  30. data/lib/dm-core/core_ext/symbol.rb +10 -0
  31. data/lib/dm-core/identity_map.rb +4 -34
  32. data/lib/dm-core/migrations.rb +1283 -0
  33. data/lib/dm-core/model.rb +570 -369
  34. data/lib/dm-core/model/descendant_set.rb +81 -0
  35. data/lib/dm-core/model/hook.rb +45 -0
  36. data/lib/dm-core/model/is.rb +32 -0
  37. data/lib/dm-core/model/property.rb +247 -0
  38. data/lib/dm-core/model/relationship.rb +335 -0
  39. data/lib/dm-core/model/scope.rb +90 -0
  40. data/lib/dm-core/property.rb +808 -273
  41. data/lib/dm-core/property_set.rb +141 -98
  42. data/lib/dm-core/query.rb +1037 -483
  43. data/lib/dm-core/query/conditions/comparison.rb +872 -0
  44. data/lib/dm-core/query/conditions/operation.rb +221 -0
  45. data/lib/dm-core/query/direction.rb +43 -0
  46. data/lib/dm-core/query/operator.rb +84 -0
  47. data/lib/dm-core/query/path.rb +138 -0
  48. data/lib/dm-core/query/sort.rb +45 -0
  49. data/lib/dm-core/repository.rb +210 -94
  50. data/lib/dm-core/resource.rb +641 -421
  51. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  52. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  53. data/lib/dm-core/support/chainable.rb +22 -0
  54. data/lib/dm-core/support/deprecate.rb +12 -0
  55. data/lib/dm-core/support/logger.rb +13 -0
  56. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  57. data/lib/dm-core/transaction.rb +333 -92
  58. data/lib/dm-core/type.rb +98 -60
  59. data/lib/dm-core/types/boolean.rb +1 -1
  60. data/lib/dm-core/types/discriminator.rb +34 -20
  61. data/lib/dm-core/types/object.rb +7 -4
  62. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  63. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  64. data/lib/dm-core/types/serial.rb +3 -3
  65. data/lib/dm-core/types/text.rb +3 -4
  66. data/lib/dm-core/version.rb +1 -1
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/migrations_spec.rb +359 -0
  80. data/spec/public/model/relationship_spec.rb +924 -0
  81. data/spec/public/model_spec.rb +159 -0
  82. data/spec/public/property_spec.rb +829 -0
  83. data/spec/public/resource_spec.rb +71 -0
  84. data/spec/public/sel_spec.rb +44 -0
  85. data/spec/public/setup_spec.rb +145 -0
  86. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  87. data/spec/public/shared/collection_shared_spec.rb +1670 -0
  88. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  89. data/spec/public/shared/resource_shared_spec.rb +924 -0
  90. data/spec/public/shared/sel_shared_spec.rb +112 -0
  91. data/spec/public/transaction_spec.rb +129 -0
  92. data/spec/public/types/discriminator_spec.rb +130 -0
  93. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  94. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  95. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  96. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  97. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  99. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  100. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  101. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  102. data/spec/semipublic/associations_spec.rb +177 -0
  103. data/spec/semipublic/collection_spec.rb +142 -0
  104. data/spec/semipublic/property_spec.rb +61 -0
  105. data/spec/semipublic/query/conditions_spec.rb +528 -0
  106. data/spec/semipublic/query/path_spec.rb +443 -0
  107. data/spec/semipublic/query_spec.rb +2626 -0
  108. data/spec/semipublic/resource_spec.rb +47 -0
  109. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  110. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  111. data/spec/spec.opts +3 -1
  112. data/spec/spec_helper.rb +80 -57
  113. data/tasks/ci.rb +19 -31
  114. data/tasks/dm.rb +43 -48
  115. data/tasks/doc.rb +8 -11
  116. data/tasks/gemspec.rb +5 -5
  117. data/tasks/hoe.rb +15 -16
  118. data/tasks/install.rb +8 -10
  119. metadata +74 -111
  120. data/lib/dm-core/associations.rb +0 -207
  121. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  122. data/lib/dm-core/auto_migrations.rb +0 -105
  123. data/lib/dm-core/dependency_queue.rb +0 -32
  124. data/lib/dm-core/hook.rb +0 -11
  125. data/lib/dm-core/is.rb +0 -16
  126. data/lib/dm-core/logger.rb +0 -232
  127. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  128. data/lib/dm-core/migrator.rb +0 -29
  129. data/lib/dm-core/scope.rb +0 -58
  130. data/lib/dm-core/support.rb +0 -7
  131. data/lib/dm-core/support/array.rb +0 -13
  132. data/lib/dm-core/support/assertions.rb +0 -8
  133. data/lib/dm-core/support/errors.rb +0 -23
  134. data/lib/dm-core/support/kernel.rb +0 -11
  135. data/lib/dm-core/support/symbol.rb +0 -41
  136. data/lib/dm-core/type_map.rb +0 -80
  137. data/lib/dm-core/types.rb +0 -19
  138. data/script/all +0 -4
  139. data/spec/integration/association_spec.rb +0 -1382
  140. data/spec/integration/association_through_spec.rb +0 -203
  141. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  142. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  143. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  144. data/spec/integration/auto_migrations_spec.rb +0 -413
  145. data/spec/integration/collection_spec.rb +0 -1073
  146. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  147. data/spec/integration/dependency_queue_spec.rb +0 -46
  148. data/spec/integration/model_spec.rb +0 -197
  149. data/spec/integration/mysql_adapter_spec.rb +0 -85
  150. data/spec/integration/postgres_adapter_spec.rb +0 -731
  151. data/spec/integration/property_spec.rb +0 -253
  152. data/spec/integration/query_spec.rb +0 -514
  153. data/spec/integration/repository_spec.rb +0 -61
  154. data/spec/integration/resource_spec.rb +0 -513
  155. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  156. data/spec/integration/sti_spec.rb +0 -273
  157. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  158. data/spec/integration/transaction_spec.rb +0 -75
  159. data/spec/integration/type_spec.rb +0 -275
  160. data/spec/lib/logging_helper.rb +0 -18
  161. data/spec/lib/mock_adapter.rb +0 -27
  162. data/spec/lib/model_loader.rb +0 -100
  163. data/spec/lib/publicize_methods.rb +0 -28
  164. data/spec/models/content.rb +0 -16
  165. data/spec/models/vehicles.rb +0 -34
  166. data/spec/models/zoo.rb +0 -48
  167. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  168. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  169. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  170. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  171. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  172. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  173. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  174. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  175. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  176. data/spec/unit/associations/relationship_spec.rb +0 -71
  177. data/spec/unit/associations_spec.rb +0 -242
  178. data/spec/unit/auto_migrations_spec.rb +0 -111
  179. data/spec/unit/collection_spec.rb +0 -182
  180. data/spec/unit/data_mapper_spec.rb +0 -35
  181. data/spec/unit/identity_map_spec.rb +0 -126
  182. data/spec/unit/is_spec.rb +0 -80
  183. data/spec/unit/migrator_spec.rb +0 -33
  184. data/spec/unit/model_spec.rb +0 -321
  185. data/spec/unit/naming_conventions_spec.rb +0 -36
  186. data/spec/unit/property_set_spec.rb +0 -90
  187. data/spec/unit/property_spec.rb +0 -753
  188. data/spec/unit/query_spec.rb +0 -571
  189. data/spec/unit/repository_spec.rb +0 -93
  190. data/spec/unit/resource_spec.rb +0 -649
  191. data/spec/unit/scope_spec.rb +0 -142
  192. data/spec/unit/transaction_spec.rb +0 -493
  193. data/spec/unit/type_map_spec.rb +0 -114
  194. data/spec/unit/type_spec.rb +0 -119
@@ -0,0 +1,81 @@
1
+ module DataMapper
2
+ module Model
3
+ class DescendantSet
4
+ include Enumerable
5
+
6
+ # Append a model as a descendant
7
+ #
8
+ # @param [Model] model
9
+ # the descendant model
10
+ #
11
+ # @return [DescendantSet]
12
+ # self
13
+ #
14
+ # @api private
15
+ def <<(model)
16
+ @descendants << model unless @descendants.include?(model)
17
+ @ancestors << model if @ancestors
18
+ self
19
+ end
20
+
21
+ # Iterate over each descendant
22
+ #
23
+ # @yield [model]
24
+ # iterate over each descendant
25
+ # @yieldparam [Model] model
26
+ # the descendant model
27
+ #
28
+ # @return [DescendantSet]
29
+ # self
30
+ #
31
+ # @api private
32
+ def each
33
+ @descendants.each { |model| yield model }
34
+ self
35
+ end
36
+
37
+ # Remove a descendant
38
+ #
39
+ # Also removed the descendant from the ancestors.
40
+ #
41
+ # @param [Model] model
42
+ # the model to remove
43
+ #
44
+ # @return [Model, NilClass]
45
+ # the model is return if it is a descendant
46
+ #
47
+ # @api private
48
+ def delete(model)
49
+ @ancestors.delete(model) if @ancestors
50
+ @descendants.delete(model)
51
+ end
52
+
53
+ # Return an Array representation of descendants
54
+ #
55
+ # @return [Array]
56
+ # the descendants
57
+ #
58
+ # @api private
59
+ def to_ary
60
+ @descendants.dup
61
+ end
62
+
63
+ private
64
+
65
+ # Initialize a DescendantSet instance
66
+ #
67
+ # @param [Model] model
68
+ # the base model
69
+ # @param [DescendantSet] ancestors
70
+ # the ancestors to notify when a descendant is added
71
+ #
72
+ # @api private
73
+ def initialize(model = nil, ancestors = nil)
74
+ @descendants = []
75
+ @ancestors = ancestors
76
+
77
+ @descendants << model if model
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,45 @@
1
+ module DataMapper
2
+ module Model
3
+ module Hook
4
+ Model.append_inclusions self
5
+
6
+ def self.included(model)
7
+ model.send(:include, Extlib::Hook)
8
+ model.extend Methods
9
+ model.register_instance_hooks :create_hook, :update_hook, :destroy
10
+ end
11
+
12
+ module Methods
13
+ # TODO: document
14
+ # @api public
15
+ def before(target_method, *args, &block)
16
+ remap_target_method(target_method).each do |target_method|
17
+ super(target_method, *args, &block)
18
+ end
19
+ end
20
+
21
+ # TODO: document
22
+ # @api public
23
+ def after(target_method, *args, &block)
24
+ remap_target_method(target_method).each do |target_method|
25
+ super(target_method, *args, &block)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # TODO: document
32
+ # @api private
33
+ def remap_target_method(target_method)
34
+ case target_method
35
+ when :create then [ :create_hook ]
36
+ when :update then [ :update_hook ]
37
+ when :save then [ :create_hook, :update_hook ]
38
+ else [ target_method ]
39
+ end
40
+ end
41
+ end
42
+
43
+ end # module Hook
44
+ end # module Model
45
+ end # module DataMapper
@@ -0,0 +1,32 @@
1
+ module DataMapper
2
+ module Model
3
+ # Module that provides a common way for plugin authors
4
+ # to implement "is ... " traits (object behaviors that can be shared)
5
+ module Is
6
+ # A common interface to activate plugins for a resource. For instance:
7
+ #
8
+ # class Widget
9
+ # include DataMapper::Resource
10
+ #
11
+ # is :list
12
+ # end
13
+ #
14
+ # adds list item behavior to the model. Plugin that wants to conform
15
+ # to "is API" of DataMapper must supply is_+behavior name+ method,
16
+ # for example above it would be is_list.
17
+ #
18
+ # @api public
19
+ def is(plugin, *args, &block)
20
+ generator_method = "is_#{plugin}".to_sym
21
+
22
+ if respond_to?(generator_method)
23
+ send(generator_method, *args, &block)
24
+ else
25
+ raise PluginNotFoundError, "could not find plugin named #{plugin}"
26
+ end
27
+ end
28
+ end # module Is
29
+
30
+ include Is
31
+ end # module Model
32
+ end # module DataMapper
@@ -0,0 +1,247 @@
1
+ # TODO: move paranoid property concerns to a ParanoidModel that is mixed
2
+ # into Model when a Paranoid property is used
3
+
4
+ # TODO: update Model#respond_to? to return true if method_method missing
5
+ # would handle the message
6
+
7
+ module DataMapper
8
+ module Model
9
+ module Property
10
+ Model.append_extensions self
11
+
12
+ extend Chainable
13
+
14
+ def self.extended(model)
15
+ model.instance_variable_set(:@properties, {})
16
+ model.instance_variable_set(:@field_naming_conventions, {})
17
+ model.instance_variable_set(:@paranoid_properties, {})
18
+ end
19
+
20
+ chainable do
21
+ def inherited(model)
22
+ model.instance_variable_set(:@properties, {})
23
+ model.instance_variable_set(:@field_naming_conventions, @field_naming_conventions.dup)
24
+ model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup)
25
+
26
+ @properties.each do |repository_name, properties|
27
+ properties.each do |property|
28
+ model.properties(repository_name) << property
29
+ end
30
+ end
31
+
32
+ super
33
+ end
34
+ end
35
+
36
+ # Defines a Property on the Resource
37
+ #
38
+ # @param [Symbol] name
39
+ # the name for which to call this property
40
+ # @param [Type] type
41
+ # the type to define this property ass
42
+ # @param [Hash(Symbol => String)] options
43
+ # a hash of available options
44
+ #
45
+ # @return [Property]
46
+ # the created Property
47
+ #
48
+ # @see Property
49
+ #
50
+ # @api public
51
+ def property(name, type, options = {})
52
+ property = DataMapper::Property.new(self, name, type, options)
53
+
54
+ properties(repository_name) << property
55
+
56
+ # Add property to the other mappings as well if this is for the default
57
+ # repository.
58
+ if repository_name == default_repository_name
59
+ @properties.except(repository_name).each do |repository_name, properties|
60
+ next if properties.named?(name)
61
+
62
+ # make sure the property is created within the correct repository scope
63
+ DataMapper.repository(repository_name) do
64
+ properties << DataMapper::Property.new(self, name, type, options)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Add the property to the lazy_loads set for this resources repository
70
+ # only.
71
+ # TODO Is this right or should we add the lazy contexts to all
72
+ # repositories?
73
+ if property.lazy?
74
+ context = options.fetch(:lazy, :default)
75
+ context = :default if context == true
76
+
77
+ Array(context).each do |item|
78
+ properties(repository_name).lazy_context(item) << name
79
+ end
80
+ end
81
+
82
+ # add the property to the child classes only if the property was
83
+ # added after the child classes' properties have been copied from
84
+ # the parent
85
+ descendants.each do |descendant|
86
+ next if descendant.properties(repository_name).named?(name)
87
+ descendant.property(name, type, options)
88
+ end
89
+
90
+ create_reader_for(property)
91
+ create_writer_for(property)
92
+
93
+ property
94
+ end
95
+
96
+ # Gets a list of all properties that have been defined on this Model in
97
+ # the requested repository
98
+ #
99
+ # @param [Symbol, String] repository_name
100
+ # The name of the repository to use. Uses the default Repository
101
+ # if none is specified.
102
+ #
103
+ # @return [Array]
104
+ # A list of Properties defined on this Model in the given Repository
105
+ #
106
+ # @api public
107
+ def properties(repository_name = default_repository_name)
108
+ # TODO: create PropertySet#copy that will copy the properties, but assign the
109
+ # new Relationship objects to a supplied repository and model. dup does not really
110
+ # do what is needed
111
+
112
+ @properties[repository_name] ||= if repository_name == default_repository_name
113
+ PropertySet.new
114
+ else
115
+ properties(default_repository_name).dup
116
+ end
117
+ end
118
+
119
+ # Gets the list of key fields for this Model in +repository_name+
120
+ #
121
+ # @param [String] repository_name
122
+ # The name of the Repository for which the key is to be reported
123
+ #
124
+ # @return [Array]
125
+ # The list of key fields for this Model in +repository_name+
126
+ #
127
+ # @api public
128
+ def key(repository_name = default_repository_name)
129
+ properties(repository_name).key
130
+ end
131
+
132
+ # TODO: document
133
+ # @api public
134
+ def serial(repository_name = default_repository_name)
135
+ key(repository_name).detect { |property| property.serial? }
136
+ end
137
+
138
+ # Gets the field naming conventions for this resource in the given Repository
139
+ #
140
+ # @param [String, Symbol] repository_name
141
+ # the name of the Repository for which the field naming convention
142
+ # will be retrieved
143
+ #
144
+ # @return [#call]
145
+ # The naming convention for the given Repository
146
+ #
147
+ # @api public
148
+ def field_naming_convention(repository_name = default_storage_name)
149
+ @field_naming_conventions[repository_name] ||= repository(repository_name).adapter.field_naming_convention
150
+ end
151
+
152
+ # TODO: document
153
+ # @api private
154
+ def properties_with_subclasses(repository_name = default_repository_name)
155
+ properties = PropertySet.new
156
+
157
+ descendants.each do |model|
158
+ model.properties(repository_name).each do |property|
159
+ properties << property unless properties.named?(property.name)
160
+ end
161
+ end
162
+
163
+ properties
164
+ end
165
+
166
+ # TODO: document
167
+ # @api private
168
+ def paranoid_properties
169
+ @paranoid_properties
170
+ end
171
+
172
+ # TODO: document
173
+ # @api private
174
+ def set_paranoid_property(name, &block)
175
+ paranoid_properties[name] = block
176
+ end
177
+
178
+ # TODO: document
179
+ # @api private
180
+ def key_conditions(repository, key)
181
+ self.key(repository.name).zip(key).to_hash
182
+ end
183
+
184
+ private
185
+
186
+ # defines the reader method for the property
187
+ #
188
+ # @api private
189
+ def create_reader_for(property)
190
+ name = property.name.to_s
191
+ reader_visibility = property.reader_visibility
192
+ instance_variable_name = property.instance_variable_name
193
+ primitive = property.primitive
194
+
195
+ unless resource_method_defined?(name)
196
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
197
+ #{reader_visibility}
198
+ def #{name}
199
+ return #{instance_variable_name} if defined?(#{instance_variable_name})
200
+ #{instance_variable_name} = properties[#{name.inspect}].get(self)
201
+ end
202
+ RUBY
203
+ end
204
+
205
+ boolean_reader_name = "#{name}?"
206
+
207
+ if primitive == TrueClass && !resource_method_defined?(boolean_reader_name)
208
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
209
+ #{reader_visibility}
210
+ alias #{boolean_reader_name} #{name}
211
+ RUBY
212
+ end
213
+ end
214
+
215
+ # defines the setter for the property
216
+ #
217
+ # @api private
218
+ def create_writer_for(property)
219
+ name = property.name
220
+ writer_visibility = property.writer_visibility
221
+
222
+ writer_name = "#{name}="
223
+
224
+ return if resource_method_defined?(writer_name)
225
+
226
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
227
+ #{writer_visibility}
228
+ def #{writer_name}(value)
229
+ properties[#{name.inspect}].set(self, value)
230
+ end
231
+ RUBY
232
+ end
233
+
234
+ chainable do
235
+ # TODO: document
236
+ # @api public
237
+ def method_missing(method, *args, &block)
238
+ if property = properties(repository_name)[method]
239
+ return property
240
+ end
241
+
242
+ super
243
+ end
244
+ end
245
+ end # module Property
246
+ end # module Model
247
+ end # module DataMapper
@@ -0,0 +1,335 @@
1
+ # TODO: update Model#respond_to? to return true if method_method missing
2
+ # would handle the message
3
+
4
+ module DataMapper
5
+ module Model
6
+ module Relationship
7
+ Model.append_extensions self
8
+
9
+ include Extlib::Assertions
10
+ extend Chainable
11
+
12
+ # Initializes relationships hash for extended model
13
+ # class.
14
+ #
15
+ # When model calls has n, has 1 or belongs_to, relationships
16
+ # are stored in that hash: keys are repository names and
17
+ # values are relationship sets.
18
+ #
19
+ # @api private
20
+ def self.extended(model)
21
+ model.instance_variable_set(:@relationships, {})
22
+ end
23
+
24
+ chainable do
25
+ # When DataMapper model is inherited, relationships
26
+ # of parent are duplicated and copied to subclass model
27
+ #
28
+ # @api private
29
+ def inherited(model)
30
+ # TODO: Create a RelationshipSet class, and then add a method that allows copying the relationships to the supplied repository and model
31
+ model.instance_variable_set(:@relationships, duped_relationships = {})
32
+
33
+ @relationships.each do |repository_name, relationships|
34
+ dup = duped_relationships[repository_name] ||= Mash.new
35
+
36
+ relationships.each do |name, relationship|
37
+ dup[name] = relationship.inherited_by(model)
38
+ end
39
+ end
40
+
41
+ super
42
+ end
43
+ end
44
+
45
+ # Returns copy of relationships set in given repository.
46
+ #
47
+ # @param [Symbol] repository_name
48
+ # Name of the repository for which relationships set is returned
49
+ # @return [Mash] relationships set for given repository
50
+ #
51
+ # @api semipublic
52
+ def relationships(repository_name = default_repository_name)
53
+ # TODO: create RelationshipSet#copy that will copy the relationships, but assign the
54
+ # new Relationship objects to a supplied repository and model. dup does not really
55
+ # do what is needed
56
+
57
+ @relationships[repository_name] ||= if repository_name == default_repository_name
58
+ Mash.new
59
+ else
60
+ relationships(default_repository_name).dup
61
+ end
62
+ end
63
+
64
+ # Used to express unlimited cardinality of association,
65
+ # see +has+
66
+ #
67
+ # @api public
68
+ def n
69
+ 1.0/0
70
+ end
71
+
72
+ # A shorthand, clear syntax for defining one-to-one, one-to-many and
73
+ # many-to-many resource relationships.
74
+ #
75
+ # * has 1, :friend # one friend
76
+ # * has n, :friends # many friends
77
+ # * has 1..3, :friends # many friends (at least 1, at most 3)
78
+ # * has 3, :friends # many friends (exactly 3)
79
+ # * has 1, :friend, 'User' # one friend with the class User
80
+ # * has 3, :friends, :through => :friendships # many friends through the friendships relationship
81
+ #
82
+ # @param cardinality [Integer, Range, Infinity]
83
+ # cardinality that defines the association type and constraints
84
+ # @param name [Symbol]
85
+ # the name that the association will be referenced by
86
+ # @param model [Model, #to_str]
87
+ # the target model of the relationship
88
+ # @param opts [Hash]
89
+ # an options hash
90
+ #
91
+ # @option :through[Symbol] A association that this join should go through to form
92
+ # a many-to-many association
93
+ # @option :model[Model, String] The name of the class to associate with, if omitted
94
+ # then the association name is assumed to match the class name
95
+ # @option :repository[Symbol]
96
+ # name of child model repository
97
+ #
98
+ # @return [Association::Relationship] the relationship that was
99
+ # created to reflect either a one-to-one, one-to-many or many-to-many
100
+ # relationship
101
+ # @raise [ArgumentError] if the cardinality was not understood. Should be a
102
+ # Integer, Range or Infinity(n)
103
+ #
104
+ # @api public
105
+ def has(cardinality, name, *args)
106
+ assert_kind_of 'cardinality', cardinality, Integer, Range, n.class
107
+ assert_kind_of 'name', name, Symbol
108
+
109
+ model = extract_model(args)
110
+ options = extract_options(args)
111
+
112
+ min, max = extract_min_max(cardinality)
113
+ options.update(:min => min, :max => max)
114
+
115
+ assert_valid_options(options)
116
+
117
+ if options.key?(:model) && model
118
+ raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument'
119
+ end
120
+
121
+ model ||= options.delete(:model)
122
+
123
+ # TODO: change to :target_respository_name and :source_repository_name
124
+ options[:child_repository_name] = options.delete(:repository)
125
+ options[:parent_repository_name] = repository.name
126
+
127
+ klass = if options[:max] > 1
128
+ options.key?(:through) ? Associations::ManyToMany::Relationship : Associations::OneToMany::Relationship
129
+ else
130
+ Associations::OneToOne::Relationship
131
+ end
132
+
133
+ relationship = relationships(repository.name)[name] = klass.new(name, model, self, options)
134
+
135
+ descendants.each do |descendant|
136
+ descendant.relationships(repository.name)[name] ||= relationship.inherited_by(descendant)
137
+ end
138
+
139
+ relationship
140
+ end
141
+
142
+ # A shorthand, clear syntax for defining many-to-one resource relationships.
143
+ #
144
+ # * belongs_to :user # many to one user
145
+ # * belongs_to :friend, :model => 'User' # many to one friend
146
+ # * belongs_to :reference, :repository => :pubmed # association for repository other than default
147
+ #
148
+ # @param name [Symbol]
149
+ # the name that the association will be referenced by
150
+ # @param model [Model, #to_str]
151
+ # the target model of the relationship
152
+ # @param opts [Hash]
153
+ # an options hash
154
+ #
155
+ # @option :model[Model, String] The name of the class to associate with, if omitted
156
+ # then the association name is assumed to match the class name
157
+ # @option :repository[Symbol]
158
+ # name of child model repository
159
+ #
160
+ # @return [Association::Relationship] The association created
161
+ # should not be accessed directly
162
+ #
163
+ # @api public
164
+ def belongs_to(name, *args)
165
+ assert_kind_of 'name', name, Symbol
166
+
167
+ model = extract_model(args)
168
+ options = extract_options(args)
169
+
170
+ if options.key?(:through)
171
+ warn "#{self.name}#belongs_to with :through is deprecated, use 'has 1, :#{name}, #{options.inspect}' in #{self.name} instead (#{caller[0]})"
172
+ return has(1, name, model, options)
173
+ end
174
+
175
+ assert_valid_options(options)
176
+
177
+ if options.key?(:model) && model
178
+ raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument'
179
+ end
180
+
181
+ model ||= options.delete(:model)
182
+
183
+ repository_name = repository.name
184
+
185
+ # TODO: change to source_repository_name and target_respository_name
186
+ options[:child_repository_name] = repository_name
187
+ options[:parent_repository_name] = options.delete(:repository)
188
+
189
+ relationship = relationships(repository.name)[name] = Associations::ManyToOne::Relationship.new(name, self, model, options)
190
+
191
+ descendants.each do |descendant|
192
+ descendant.relationships(repository.name)[name] ||= relationship.inherited_by(descendant)
193
+ end
194
+
195
+ relationship
196
+ end
197
+
198
+ private
199
+
200
+ # Extract the model from an Array of arguments
201
+ #
202
+ # @param [Array(Model, String, Hash)]
203
+ # The arguments passed to an relationship declaration
204
+ #
205
+ # @return [Model, #to_str]
206
+ # target model for the association
207
+ #
208
+ # @api private
209
+ def extract_model(args)
210
+ model = args.first
211
+
212
+ if model.kind_of?(Model)
213
+ model
214
+ elsif model.respond_to?(:to_str)
215
+ model.to_str
216
+ else
217
+ nil
218
+ end
219
+ end
220
+
221
+ # Extract the model from an Array of arguments
222
+ #
223
+ # @param [Array(Model, String, Hash)]
224
+ # The arguments passed to an relationship declaration
225
+ #
226
+ # @return [Hash]
227
+ # options for the association
228
+ #
229
+ # @api private
230
+ def extract_options(args)
231
+ options = args.last
232
+
233
+ if options.kind_of?(Hash)
234
+ options.dup
235
+ else
236
+ {}
237
+ end
238
+ end
239
+
240
+ # A support method for converting Integer, Range or Infinity values into two
241
+ # values representing the minimum and maximum cardinality of the association
242
+ #
243
+ # @return [Array] A pair of integers, min and max
244
+ #
245
+ # @api private
246
+ def extract_min_max(cardinality)
247
+ case cardinality
248
+ when Integer then [ cardinality, cardinality ]
249
+ when Range then [ cardinality.first, cardinality.last ]
250
+ when n then [ 0, n ]
251
+ end
252
+ end
253
+
254
+ # Validates options of association method like belongs_to or has:
255
+ # verifies types of cardinality bounds, repository, association class,
256
+ # keys and possible values of :through option.
257
+ #
258
+ # @api private
259
+ def assert_valid_options(options)
260
+ # TODO: update to match Query#assert_valid_options
261
+ # - perform options normalization elsewhere
262
+
263
+ if options.key?(:min) && options.key?(:max)
264
+ assert_kind_of 'options[:min]', options[:min], Integer
265
+ assert_kind_of 'options[:max]', options[:max], Integer, n.class
266
+
267
+ if options[:min] == n && options[:max] == n
268
+ raise ArgumentError, 'Cardinality may not be n..n. The cardinality specifies the min/max number of results from the association'
269
+ elsif options[:min] > options[:max]
270
+ raise ArgumentError, "Cardinality min (#{options[:min]}) cannot be larger than the max (#{options[:max]})"
271
+ elsif options[:min] < 0
272
+ raise ArgumentError, "Cardinality min much be greater than or equal to 0, but was #{options[:min]}"
273
+ elsif options[:max] < 1
274
+ raise ArgumentError, "Cardinality max much be greater than or equal to 1, but was #{options[:max]}"
275
+ end
276
+ end
277
+
278
+ if options.key?(:repository)
279
+ assert_kind_of 'options[:repository]', options[:repository], Repository, Symbol
280
+
281
+ if options[:repository].kind_of?(Repository)
282
+ options[:repository] = options[:repository].name
283
+ end
284
+ end
285
+
286
+ if options.key?(:class_name)
287
+ assert_kind_of 'options[:class_name]', options[:class_name], String
288
+ warn "+options[:class_name]+ is deprecated, use :model instead (#{caller[1]})"
289
+ options[:model] = options.delete(:class_name)
290
+ end
291
+
292
+ if options.key?(:remote_name)
293
+ assert_kind_of 'options[:remote_name]', options[:remote_name], Symbol
294
+ warn "+options[:remote_name]+ is deprecated, use :via instead (#{caller[1]})"
295
+ options[:via] = options.delete(:remote_name)
296
+ end
297
+
298
+ if options.key?(:through)
299
+ assert_kind_of 'options[:through]', options[:through], Symbol, Module
300
+ end
301
+
302
+ [ :via, :inverse ].each do |key|
303
+ if options.key?(key)
304
+ assert_kind_of "options[#{key.inspect}]", options[key], Symbol, Associations::Relationship
305
+ end
306
+ end
307
+
308
+ # TODO: deprecate :child_key and :parent_key in favor of :source_key and
309
+ # :target_key (will mean something different for each relationship)
310
+
311
+ [ :child_key, :parent_key ].each do |key|
312
+ if options.key?(key)
313
+ assert_kind_of "options[#{key.inspect}]", options[key], Enumerable
314
+ end
315
+ end
316
+
317
+ if options.key?(:limit)
318
+ raise ArgumentError, '+options[:limit]+ should not be specified on a relationship'
319
+ end
320
+ end
321
+
322
+ chainable do
323
+ # TODO: document
324
+ # @api public
325
+ def method_missing(method, *args, &block)
326
+ if relationship = relationships(repository_name)[method]
327
+ return Query::Path.new([ relationship ])
328
+ end
329
+
330
+ super
331
+ end
332
+ end
333
+ end # module Relationship
334
+ end # module Model
335
+ end # module DataMapper