mongoid 1.1.3 → 1.1.4

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