mongoid 1.1.3 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.watchr +20 -2
  2. data/VERSION +1 -1
  3. data/caliper.yml +4 -0
  4. data/lib/mongoid.rb +0 -1
  5. data/lib/mongoid/associations.rb +25 -13
  6. data/lib/mongoid/associations/belongs_to.rb +15 -17
  7. data/lib/mongoid/associations/belongs_to_related.rb +12 -14
  8. data/lib/mongoid/associations/has_many.rb +53 -35
  9. data/lib/mongoid/associations/has_many_related.rb +10 -15
  10. data/lib/mongoid/associations/has_one.rb +31 -30
  11. data/lib/mongoid/associations/has_one_related.rb +18 -20
  12. data/lib/mongoid/associations/options.rb +10 -0
  13. data/lib/mongoid/associations/proxy.rb +18 -1
  14. data/lib/mongoid/criteria.rb +9 -233
  15. data/lib/mongoid/criterion/complex.rb +21 -0
  16. data/lib/mongoid/criterion/exclusion.rb +63 -0
  17. data/lib/mongoid/criterion/inclusion.rb +91 -0
  18. data/lib/mongoid/criterion/optional.rb +96 -0
  19. data/lib/mongoid/document.rb +2 -2
  20. data/lib/mongoid/extensions/hash/accessors.rb +6 -0
  21. data/lib/mongoid/extensions/hash/criteria_helpers.rb +2 -2
  22. data/lib/mongoid/extensions/symbol/inflections.rb +1 -1
  23. data/mongoid.gemspec +53 -3
  24. data/spec/integration/mongoid/associations_spec.rb +41 -0
  25. data/spec/models/address.rb +39 -0
  26. data/spec/models/animal.rb +6 -0
  27. data/spec/models/comment.rb +8 -0
  28. data/spec/models/country_code.rb +6 -0
  29. data/spec/models/employer.rb +5 -0
  30. data/spec/models/game.rb +6 -0
  31. data/spec/models/inheritance.rb +56 -0
  32. data/spec/models/location.rb +5 -0
  33. data/spec/models/mixed_drink.rb +4 -0
  34. data/spec/models/name.rb +13 -0
  35. data/spec/models/namespacing.rb +11 -0
  36. data/spec/models/patient.rb +4 -0
  37. data/spec/models/person.rb +97 -0
  38. data/spec/models/pet.rb +7 -0
  39. data/spec/models/pet_owner.rb +6 -0
  40. data/spec/models/phone.rb +7 -0
  41. data/spec/models/post.rb +15 -0
  42. data/spec/models/translation.rb +5 -0
  43. data/spec/models/vet_visit.rb +5 -0
  44. data/spec/spec_helper.rb +9 -326
  45. data/spec/unit/mongoid/associations/belongs_to_related_spec.rb +26 -5
  46. data/spec/unit/mongoid/associations/belongs_to_spec.rb +96 -30
  47. data/spec/unit/mongoid/associations/has_many_related_spec.rb +32 -12
  48. data/spec/unit/mongoid/associations/has_many_spec.rb +48 -12
  49. data/spec/unit/mongoid/associations/has_one_related_spec.rb +29 -4
  50. data/spec/unit/mongoid/associations/has_one_spec.rb +46 -1
  51. data/spec/unit/mongoid/associations/options_spec.rb +58 -0
  52. data/spec/unit/mongoid/associations_spec.rb +58 -1
  53. data/spec/unit/mongoid/criteria_spec.rb +71 -735
  54. data/spec/unit/mongoid/criterion/complex_spec.rb +19 -0
  55. data/spec/unit/mongoid/criterion/exclusion_spec.rb +75 -0
  56. data/spec/unit/mongoid/criterion/inclusion_spec.rb +213 -0
  57. data/spec/unit/mongoid/criterion/optional_spec.rb +244 -0
  58. data/spec/unit/mongoid/extensions/hash/accessors_spec.rb +20 -0
  59. metadata +53 -3
  60. data/lib/mongoid/complex_criterion.rb +0 -10
@@ -4,7 +4,10 @@ module Mongoid #:nodoc:
4
4
  class HasOne #:nodoc:
5
5
  include Proxy
6
6
 
7
- attr_reader :association_name, :document, :parent, :options
7
+ # Build a new object for the association.
8
+ def build(attrs = {}, type = nil)
9
+ @target = attrs.assimilate(@parent, @options, type); self
10
+ end
8
11
 
9
12
  # Creates the new association by finding the attributes in
10
13
  # the parent document with its name, and instantiating a
@@ -16,43 +19,32 @@ module Mongoid #:nodoc:
16
19
  # Options:
17
20
  #
18
21
  # document: The parent +Document+
19
- # attributes: The attributes of the decorated object.
22
+ # attributes: The attributes of the target object.
20
23
  # options: The association options.
21
- def initialize(document, attributes, options)
22
- @parent, @options, @association_name = document, options, options.name
23
- attrs = attributes.stringify_keys
24
- klass = attrs["_type"] ? attrs["_type"].constantize : nil
25
- @document = attrs.assimilate(@parent, @options, klass)
26
- end
27
-
28
- # Delegate all missing methods over to the +Document+.
29
- def method_missing(name, *args, &block)
30
- @document.send(name, *args, &block)
24
+ #
25
+ # Returns:
26
+ #
27
+ # A new +HashOne+ association proxy.
28
+ def initialize(document, attrs, options)
29
+ @parent, @options = document, options
30
+ @target = attrs.assimilate(@parent, @options, attrs.klass)
31
+ extends(options)
31
32
  end
32
33
 
33
34
  # Used for setting the association via a nested attributes setter on the
34
- # parent +Document+.
35
+ # parent +Document+. Called when using accepts_nested_attributes_for.
36
+ #
37
+ # Options:
38
+ #
39
+ # attributes: The attributes for the new association
40
+ #
41
+ # Returns:
42
+ #
43
+ # A new target document.
35
44
  def nested_build(attributes)
36
45
  build(attributes)
37
46
  end
38
47
 
39
- # This will get deprecated
40
- def to_a
41
- [@document]
42
- end
43
-
44
- # Need to override here for when the underlying document is nil.
45
- def valid?
46
- @document.valid?
47
- end
48
-
49
- protected
50
- # Build a new object for the association.
51
- def build(attrs = {}, type = nil)
52
- @document = attrs.assimilate(@parent, @options, type)
53
- self
54
- end
55
-
56
48
  class << self
57
49
  # Preferred method of instantiating a new +HasOne+, since nil values
58
50
  # will be handled properly.
@@ -61,6 +53,10 @@ module Mongoid #:nodoc:
61
53
  #
62
54
  # document: The parent +Document+
63
55
  # options: The association options.
56
+ #
57
+ # Returns:
58
+ #
59
+ # A new +HasOne+ association proxy.
64
60
  def instantiate(document, options)
65
61
  attributes = document.raw_attributes[options.name]
66
62
  return nil if attributes.blank?
@@ -84,8 +80,13 @@ module Mongoid #:nodoc:
84
80
  # Example:
85
81
  #
86
82
  # <tt>HasOne.update({:first_name => "Hank"}, person, options)</tt>
83
+ #
84
+ # Returns:
85
+ #
86
+ # A new +HasOne+ association proxy.
87
87
  def update(child, parent, options)
88
88
  child.assimilate(parent, options)
89
+ instantiate(parent, options)
89
90
  end
90
91
  end
91
92
 
@@ -4,17 +4,16 @@ module Mongoid #:nodoc:
4
4
  class HasOneRelated #:nodoc:
5
5
  include Proxy
6
6
 
7
- delegate :==, :nil?, :to => :document
8
- attr_reader :klass, :document
7
+ delegate :nil?, :to => :target
9
8
 
10
9
  # Builds a new Document and sets it as the association.
11
10
  #
12
11
  # Returns the newly created object.
13
12
  def build(attributes = {})
14
- @document = @klass.instantiate(attributes)
13
+ @target = @klass.instantiate(attributes)
15
14
  name = @parent.class.to_s.underscore
16
- @document.send("#{name}=", @parent)
17
- @document
15
+ @target.send("#{name}=", @parent)
16
+ @target
18
17
  end
19
18
 
20
19
  # Builds a new Document and sets it as the association, then saves the
@@ -22,7 +21,7 @@ module Mongoid #:nodoc:
22
21
  #
23
22
  # Returns the newly created object.
24
23
  def create(attributes)
25
- build(attributes); @document.save; @document
24
+ build(attributes); @target.save; @target
26
25
  end
27
26
 
28
27
  # Initializing a related association only requires looking up the objects
@@ -32,15 +31,11 @@ module Mongoid #:nodoc:
32
31
  #
33
32
  # document: The +Document+ that contains the relationship.
34
33
  # options: The association +Options+.
35
- def initialize(document, options)
34
+ def initialize(document, options, target = nil)
36
35
  @parent, @klass = document, options.klass
37
36
  @foreign_key = document.class.to_s.foreign_key
38
- @document = @klass.first(:conditions => { @foreign_key => @parent.id })
39
- end
40
-
41
- # Delegate all missing methods over to the +Document+.
42
- def method_missing(name, *args)
43
- @document.send(name, *args)
37
+ @target = target || @klass.first(:conditions => { @foreign_key => @parent.id })
38
+ extends(options)
44
39
  end
45
40
 
46
41
  class << self
@@ -50,8 +45,8 @@ module Mongoid #:nodoc:
50
45
  #
51
46
  # document: The +Document+ that contains the relationship.
52
47
  # options: The association +Options+.
53
- def instantiate(document, options)
54
- new(document, options)
48
+ def instantiate(document, options, target = nil)
49
+ new(document, options, target)
55
50
  end
56
51
 
57
52
  # Returns the macro used to create the association.
@@ -70,11 +65,14 @@ module Mongoid #:nodoc:
70
65
  #
71
66
  # Example:
72
67
  #
73
- # <tt>HasManyToRelated.update(game, person, options)</tt>
74
- def update(related, document, options)
75
- name = document.class.to_s.underscore
76
- related.send("#{name}=", document)
77
- related
68
+ # <tt>HasOneToRelated.update(game, person, options)</tt>
69
+ def update(target, document, options)
70
+ if target
71
+ name = document.class.to_s.underscore
72
+ target.send("#{name}=", document)
73
+ return instantiate(document, options, target)
74
+ end
75
+ target
78
76
  end
79
77
  end
80
78
 
@@ -9,6 +9,16 @@ module Mongoid #:nodoc:
9
9
  @attributes = attributes
10
10
  end
11
11
 
12
+ # Returns the extension if it exists, nil if not.
13
+ def extension
14
+ @attributes[:extend]
15
+ end
16
+
17
+ # Returns true is the options have extensions.
18
+ def extension?
19
+ !extension.nil?
20
+ end
21
+
12
22
  # Return the foreign key based off the association name.
13
23
  def foreign_key
14
24
  name.to_s.foreign_key
@@ -5,8 +5,25 @@ module Mongoid #:nodoc
5
5
  def self.included(base)
6
6
  base.class_eval do
7
7
  instance_methods.each do |method|
8
- undef_method(method) unless method =~ /(^__|^nil\?$|^send$|^object_id$)/
8
+ undef_method(method) unless method =~ /(^__|^nil\?$|^send$|^object_id$|^extend$)/
9
9
  end
10
+ include InstanceMethods
11
+ end
12
+ end
13
+ module InstanceMethods #:nodoc:
14
+ attr_reader \
15
+ :options,
16
+ :target
17
+
18
+ # Default behavior of method missing should be to delegate all calls
19
+ # to the target of the proxy. This can be overridden in special cases.
20
+ def method_missing(name, *args, &block)
21
+ @target.send(name, *args, &block)
22
+ end
23
+
24
+ # If anonymous extensions are added this will take care of them.
25
+ def extends(options)
26
+ extend Module.new(&options.extension) if options.extension?
10
27
  end
11
28
  end
12
29
  end
@@ -1,4 +1,9 @@
1
1
  # encoding: utf-8
2
+ require "mongoid/criterion/complex"
3
+ require "mongoid/criterion/exclusion"
4
+ require "mongoid/criterion/inclusion"
5
+ require "mongoid/criterion/optional"
6
+
2
7
  module Mongoid #:nodoc:
3
8
  # The +Criteria+ class is the core object needed in Mongoid to retrieve
4
9
  # objects from the database. It is a DSL that essentially sets up the
@@ -15,6 +20,9 @@ module Mongoid #:nodoc:
15
20
  #
16
21
  # <tt>criteria.execute</tt>
17
22
  class Criteria
23
+ include Criterion::Exclusion
24
+ include Criterion::Inclusion
25
+ include Criterion::Optional
18
26
  include Enumerable
19
27
 
20
28
  attr_accessor :documents
@@ -67,46 +75,6 @@ module Mongoid #:nodoc:
67
75
  end
68
76
  end
69
77
 
70
- # Adds a criterion to the +Criteria+ that specifies values that must all
71
- # be matched in order to return results. Similar to an "in" clause but the
72
- # underlying conditional logic is an "AND" and not an "OR". The MongoDB
73
- # conditional operator that will be used is "$all".
74
- #
75
- # Options:
76
- #
77
- # attributes: A +Hash+ where the key is the field name and the value is an
78
- # +Array+ of values that must all match.
79
- #
80
- # Example:
81
- #
82
- # <tt>criteria.all(:field => ["value1", "value2"])</tt>
83
- #
84
- # <tt>criteria.all(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
85
- #
86
- # Returns: <tt>self</tt>
87
- def all(attributes = {})
88
- update_selector(attributes, "$all")
89
- end
90
-
91
- # Adds a criterion to the +Criteria+ that specifies values that must
92
- # be matched in order to return results. This is similar to a SQL "WHERE"
93
- # clause. This is the actual selector that will be provided to MongoDB,
94
- # similar to the Javascript object that is used when performing a find()
95
- # in the MongoDB console.
96
- #
97
- # Options:
98
- #
99
- # selectior: A +Hash+ that must match the attributes of the +Document+.
100
- #
101
- # Example:
102
- #
103
- # <tt>criteria.and(:field1 => "value1", :field2 => 15)</tt>
104
- #
105
- # Returns: <tt>self</tt>
106
- def and(selector = nil)
107
- where(selector)
108
- end
109
-
110
78
  # Return or create the context in which this criteria should be executed.
111
79
  #
112
80
  # This will return an Enumerable context if the class is embedded,
@@ -143,78 +111,6 @@ module Mongoid #:nodoc:
143
111
  block_given? ? @collection.each { |doc| yield doc } : self
144
112
  end
145
113
 
146
- # Adds a criterion to the +Criteria+ that specifies values that are not allowed
147
- # to match any document in the database. The MongoDB conditional operator that
148
- # will be used is "$ne".
149
- #
150
- # Options:
151
- #
152
- # attributes: A +Hash+ where the key is the field name and the value is a
153
- # value that must not be equal to the corresponding field value in the database.
154
- #
155
- # Example:
156
- #
157
- # <tt>criteria.excludes(:field => "value1")</tt>
158
- #
159
- # <tt>criteria.excludes(:field1 => "value1", :field2 => "value1")</tt>
160
- #
161
- # Returns: <tt>self</tt>
162
- def excludes(attributes = {})
163
- update_selector(attributes, "$ne")
164
- end
165
-
166
- # Adds a criterion to the +Criteria+ that specifies additional options
167
- # to be passed to the Ruby driver, in the exact format for the driver.
168
- #
169
- # Options:
170
- #
171
- # extras: A +Hash+ that gets set to the driver options.
172
- #
173
- # Example:
174
- #
175
- # <tt>criteria.extras(:limit => 20, :skip => 40)</tt>
176
- #
177
- # Returns: <tt>self</tt>
178
- def extras(extras)
179
- @options = extras; filter_options; self
180
- end
181
-
182
- # Adds a criterion to the +Criteria+ that specifies values where any can
183
- # be matched in order to return results. This is similar to an SQL "IN"
184
- # clause. The MongoDB conditional operator that will be used is "$in".
185
- #
186
- # Options:
187
- #
188
- # attributes: A +Hash+ where the key is the field name and the value is an
189
- # +Array+ of values that any can match.
190
- #
191
- # Example:
192
- #
193
- # <tt>criteria.in(:field => ["value1", "value2"])</tt>
194
- #
195
- # <tt>criteria.in(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
196
- #
197
- # Returns: <tt>self</tt>
198
- def in(attributes = {})
199
- update_selector(attributes, "$in")
200
- end
201
-
202
- # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
203
- #
204
- # Options:
205
- #
206
- # object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
207
- #
208
- # Example:
209
- #
210
- # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
211
- #
212
- # Returns: <tt>self</tt>
213
- def id(*args)
214
- (args.flatten.size > 1) ? self.in(:_id => args.flatten) : (@selector[:_id] = *args)
215
- self
216
- end
217
-
218
114
  # Create the new +Criteria+ object. This will initialize the selector
219
115
  # and options hashes, as well as the type of criteria.
220
116
  #
@@ -230,23 +126,6 @@ module Mongoid #:nodoc:
230
126
  end
231
127
  end
232
128
 
233
- # Adds a criterion to the +Criteria+ that specifies the maximum number of
234
- # results to return. This is mostly used in conjunction with <tt>skip()</tt>
235
- # to handle paginated results.
236
- #
237
- # Options:
238
- #
239
- # value: An +Integer+ specifying the max number of results. Defaults to 20.
240
- #
241
- # Example:
242
- #
243
- # <tt>criteria.limit(100)</tt>
244
- #
245
- # Returns: <tt>self</tt>
246
- def limit(value = 20)
247
- @options[:limit] = value; self
248
- end
249
-
250
129
  # Merges another object into this +Criteria+. The other object may be a
251
130
  # +Criteria+ or a +Hash+. This is used to combine multiple scopes together,
252
131
  # where a chained scope situation may be desired.
@@ -303,65 +182,7 @@ module Mongoid #:nodoc:
303
182
  end
304
183
  end
305
184
 
306
- # Adds a criterion to the +Criteria+ that specifies values where none
307
- # should match in order to return results. This is similar to an SQL "NOT IN"
308
- # clause. The MongoDB conditional operator that will be used is "$nin".
309
- #
310
- # Options:
311
- #
312
- # exclusions: A +Hash+ where the key is the field name and the value is an
313
- # +Array+ of values that none can match.
314
- #
315
- # Example:
316
- #
317
- # <tt>criteria.not_in(:field => ["value1", "value2"])</tt>
318
- #
319
- # <tt>criteria.not_in(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
320
- #
321
- # Returns: <tt>self</tt>
322
- def not_in(exclusions)
323
- exclusions.each { |key, value| @selector[key] = { "$nin" => value } }; self
324
- end
325
-
326
- # Returns the offset option. If a per_page option is in the list then it
327
- # will replace it with a skip parameter and return the same value. Defaults
328
- # to 20 if nothing was provided.
329
- def offset
330
- @options[:skip]
331
- end
332
-
333
- # Adds a criterion to the +Criteria+ that specifies the fields that will
334
- # get returned from the Document. Used mainly for list views that do not
335
- # require all fields to be present. This is similar to SQL "SELECT" values.
336
- #
337
- # Options:
338
- #
339
- # args: A list of field names to retrict the returned fields to.
340
- #
341
- # Example:
342
- #
343
- # <tt>criteria.only(:field1, :field2, :field3)</tt>
344
- #
345
- # Returns: <tt>self</tt>
346
- def only(*args)
347
- @options[:fields] = args.flatten if args.any?; self
348
- end
349
-
350
- # Adds a criterion to the +Criteria+ that specifies the sort order of
351
- # the returned documents in the database. Similar to a SQL "ORDER BY".
352
- #
353
- # Options:
354
- #
355
- # params: An +Array+ of [field, direction] sorting pairs.
356
- #
357
- # Example:
358
- #
359
- # <tt>criteria.order_by([[:field1, :asc], [:field2, :desc]])</tt>
360
- #
361
- # Returns: <tt>self</tt>
362
- def order_by(params = [])
363
- @options[:sort] = params; self
364
- end
185
+ alias :to_ary :to_a
365
186
 
366
187
  # Returns the selector and options as a +Hash+ that would be passed to a
367
188
  # scope for use with named scopes.
@@ -369,26 +190,6 @@ module Mongoid #:nodoc:
369
190
  { :where => @selector }.merge(@options)
370
191
  end
371
192
 
372
- # Adds a criterion to the +Criteria+ that specifies how many results to skip
373
- # when returning Documents. This is mostly used in conjunction with
374
- # <tt>limit()</tt> to handle paginated results, and is similar to the
375
- # traditional "offset" parameter.
376
- #
377
- # Options:
378
- #
379
- # value: An +Integer+ specifying the number of results to skip. Defaults to 0.
380
- #
381
- # Example:
382
- #
383
- # <tt>criteria.skip(20)</tt>
384
- #
385
- # Returns: <tt>self</tt>
386
- def skip(value = 0)
387
- @options[:skip] = value; self
388
- end
389
-
390
- alias :to_ary :to_a
391
-
392
193
  # Translate the supplied arguments into a +Criteria+ object.
393
194
  #
394
195
  # If the passed in args is a single +String+, then it will
@@ -417,31 +218,6 @@ module Mongoid #:nodoc:
417
218
  return new(klass).where(params.delete(:conditions) || {}).extras(params)
418
219
  end
419
220
 
420
- # Adds a criterion to the +Criteria+ that specifies values that must
421
- # be matched in order to return results. This is similar to a SQL "WHERE"
422
- # clause. This is the actual selector that will be provided to MongoDB,
423
- # similar to the Javascript object that is used when performing a find()
424
- # in the MongoDB console.
425
- #
426
- # Options:
427
- #
428
- # selectior: A +Hash+ that must match the attributes of the +Document+.
429
- #
430
- # Example:
431
- #
432
- # <tt>criteria.where(:field1 => "value1", :field2 => 15)</tt>
433
- #
434
- # Returns: <tt>self</tt>
435
- def where(selector = nil)
436
- case selector
437
- when String
438
- @selector.update("$where" => selector)
439
- else
440
- @selector.update(selector ? selector.expand_complex_criteria : {})
441
- end
442
- self
443
- end
444
-
445
221
  protected
446
222
  # Determines the context to be used for this criteria.
447
223
  def determine_context