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.
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