mongodoc 0.2.2 → 0.2.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 (57) hide show
  1. data/Rakefile +21 -0
  2. data/TODO +6 -1
  3. data/VERSION +1 -1
  4. data/features/finders.feature +1 -1
  5. data/features/mongodoc_base.feature +11 -2
  6. data/features/{named_scopes.feature → scopes.feature} +0 -0
  7. data/features/step_definitions/document_steps.rb +15 -4
  8. data/features/step_definitions/documents.rb +3 -3
  9. data/features/step_definitions/query_steps.rb +17 -14
  10. data/features/step_definitions/{named_scope_steps.rb → scope_steps.rb} +0 -0
  11. data/features/using_criteria.feature +22 -43
  12. data/lib/mongodoc.rb +1 -1
  13. data/lib/mongodoc/associations/collection_proxy.rb +3 -1
  14. data/lib/mongodoc/associations/document_proxy.rb +4 -1
  15. data/lib/mongodoc/associations/hash_proxy.rb +3 -1
  16. data/lib/mongodoc/associations/proxy_base.rb +6 -4
  17. data/lib/mongodoc/attributes.rb +6 -6
  18. data/lib/mongodoc/contexts.rb +24 -0
  19. data/lib/mongodoc/contexts/enumerable.rb +132 -0
  20. data/lib/mongodoc/contexts/mongo.rb +215 -0
  21. data/lib/mongodoc/criteria.rb +36 -479
  22. data/lib/mongodoc/document.rb +3 -2
  23. data/lib/mongodoc/finders.rb +31 -11
  24. data/lib/mongodoc/matchers.rb +35 -0
  25. data/lib/mongodoc/scope.rb +64 -0
  26. data/lib/mongoid/contexts/paging.rb +42 -0
  27. data/lib/mongoid/criteria.rb +264 -0
  28. data/lib/mongoid/criterion/complex.rb +21 -0
  29. data/lib/mongoid/criterion/exclusion.rb +65 -0
  30. data/lib/mongoid/criterion/inclusion.rb +92 -0
  31. data/lib/mongoid/criterion/optional.rb +136 -0
  32. data/lib/mongoid/extensions/hash/criteria_helpers.rb +20 -0
  33. data/lib/mongoid/extensions/symbol/inflections.rb +36 -0
  34. data/lib/mongoid/matchers/all.rb +11 -0
  35. data/lib/mongoid/matchers/default.rb +26 -0
  36. data/lib/mongoid/matchers/exists.rb +13 -0
  37. data/lib/mongoid/matchers/gt.rb +11 -0
  38. data/lib/mongoid/matchers/gte.rb +11 -0
  39. data/lib/mongoid/matchers/in.rb +11 -0
  40. data/lib/mongoid/matchers/lt.rb +11 -0
  41. data/lib/mongoid/matchers/lte.rb +11 -0
  42. data/lib/mongoid/matchers/ne.rb +11 -0
  43. data/lib/mongoid/matchers/nin.rb +11 -0
  44. data/lib/mongoid/matchers/size.rb +11 -0
  45. data/mongodoc.gemspec +39 -9
  46. data/spec/attributes_spec.rb +16 -2
  47. data/spec/contexts/enumerable_spec.rb +335 -0
  48. data/spec/contexts/mongo_spec.rb +148 -0
  49. data/spec/contexts_spec.rb +28 -0
  50. data/spec/criteria_spec.rb +15 -766
  51. data/spec/finders_spec.rb +28 -36
  52. data/spec/matchers_spec.rb +342 -0
  53. data/spec/scope_spec.rb +79 -0
  54. metadata +40 -10
  55. data/features/step_definitions/criteria_steps.rb +0 -42
  56. data/lib/mongodoc/named_scope.rb +0 -68
  57. data/spec/named_scope_spec.rb +0 -82
@@ -3,7 +3,7 @@ require 'mongodoc/query'
3
3
  require 'mongodoc/attributes'
4
4
  require 'mongodoc/criteria'
5
5
  require 'mongodoc/finders'
6
- require 'mongodoc/named_scope'
6
+ require 'mongodoc/scope'
7
7
  require 'mongodoc/validations/macros'
8
8
 
9
9
  module MongoDoc
@@ -17,8 +17,9 @@ module MongoDoc
17
17
  klass.class_eval do
18
18
  include Attributes
19
19
  extend ClassMethods
20
+ extend Criteria
20
21
  extend Finders
21
- extend NamedScope
22
+ extend Scope
22
23
  include ::Validatable
23
24
  extend Validations::Macros
24
25
 
@@ -1,29 +1,49 @@
1
+ require 'mongodoc/criteria'
2
+
1
3
  module MongoDoc
2
4
  module Finders
3
- [:all, :count, :first, :last].each do |name|
5
+ def self.extended(base)
6
+ base.extend(Criteria) unless base === Criteria
7
+ end
8
+
9
+ %w(count first last).each do |name|
4
10
  module_eval <<-RUBY
11
+ # #{name.humanize} for this +Document+ class
12
+ #
13
+ # <tt>Person.#{name}</tt>
5
14
  def #{name}
6
- Criteria.new(self).#{name}
15
+ criteria.#{name}
7
16
  end
8
17
  RUBY
9
18
  end
10
19
 
11
- def criteria
12
- Criteria.new(self)
13
- end
14
-
20
+ # Find a +Document+ based on id (+String+ or +Mongo::ObjectID+)
21
+ #
22
+ # <tt>Person.find('1')</tt>
23
+ # <tt>Person.find(obj_id_1, obj_id_2)</tt>
15
24
  def find(*args)
16
- query = args.extract_options!
17
- which = args.first
18
- Criteria.translate(self, query).send(which)
25
+ criteria.id(*args)
26
+ end
27
+ #
28
+ # Find all +Document+s in the collections
29
+ #
30
+ # <tt>Person.find_all</tt>
31
+ def find_all
32
+ criteria
19
33
  end
20
34
 
35
+ # Find a +Document+ based on id (+String+ or +Mongo::ObjectID+)
36
+ # or conditions
37
+ #
38
+ # <tt>Person.find_one('1')</tt>
39
+ # <tt>Person.find_one(:where => {:age.gt > 25})</tt>
21
40
  def find_one(conditions_or_id)
22
41
  if Hash === conditions_or_id
23
- Criteria.translate(self, conditions_or_id).one
42
+ Mongoid::Criteria.translate(self, conditions_or_id).one
24
43
  else
25
- Criteria.translate(self, conditions_or_id)
44
+ Mongoid::Criteria.translate(self, conditions_or_id)
26
45
  end
27
46
  end
47
+
28
48
  end
29
49
  end
@@ -0,0 +1,35 @@
1
+ require "mongoid/matchers/default"
2
+ require "mongoid/matchers/all"
3
+ require "mongoid/matchers/exists"
4
+ require "mongoid/matchers/gt"
5
+ require "mongoid/matchers/gte"
6
+ require "mongoid/matchers/in"
7
+ require "mongoid/matchers/lt"
8
+ require "mongoid/matchers/lte"
9
+ require "mongoid/matchers/ne"
10
+ require "mongoid/matchers/nin"
11
+ require "mongoid/matchers/size"
12
+
13
+ module MongoDoc #:nodoc:
14
+ module Matchers
15
+ # Determines if this document has the attributes to match the supplied
16
+ # MongoDB selector. Used for matching on embedded associations.
17
+ def matches?(selector)
18
+ selector.each_pair do |key, value|
19
+ return false unless matcher(key, value).matches?(value)
20
+ end
21
+ true
22
+ end
23
+
24
+ protected
25
+ # Get the matcher for the supplied key and value. Will determine the class
26
+ # name from the key.
27
+ def matcher(key, value)
28
+ if value.is_a?(Hash)
29
+ name = "Mongoid::Matchers::#{value.keys.first.gsub("$", "").camelize}"
30
+ return name.constantize.new(send(key))
31
+ end
32
+ Mongoid::Matchers::Default.new(send(key))
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ # Based on ActiveRecord::NamedScope
2
+ module MongoDoc
3
+ module Scope
4
+ def scopes
5
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
6
+ end
7
+
8
+ def scope(name, *args, &block)
9
+ options = args.extract_options!
10
+ raise ArgumentError if args.size != 1
11
+ criteria = args.first
12
+ name = name.to_sym
13
+ scopes[name] = lambda do |parent_scope, *args|
14
+ CriteriaProxy.new(parent_scope, Mongoid::Criteria === criteria ? criteria : criteria.call(*args), options, &block)
15
+ end
16
+ (class << self; self; end).class_eval <<-EOT
17
+ def #{name}(*args)
18
+ scopes[:#{name}].call(self, *args)
19
+ end
20
+ EOT
21
+ end
22
+
23
+ class CriteriaProxy
24
+ attr_accessor :criteria, :klass, :parent_scope
25
+
26
+ delegate :scopes, :to => :parent_scope
27
+
28
+ def initialize(parent_scope, criteria, options, &block)
29
+ [options.delete(:extend)].flatten.each { |extension| extend extension } if options.include?(:extend)
30
+ extend Module.new(&block) if block_given?
31
+ if CriteriaProxy === parent_scope
32
+ chained = Mongoid::Criteria.new(klass)
33
+ chained.merge(parent_scope)
34
+ chained.merge(criteria)
35
+ self.criteria = chained
36
+ self.klass = criteria.klass
37
+ else
38
+ self.criteria = criteria
39
+ self.klass = parent_scope
40
+ end
41
+
42
+ self.parent_scope = parent_scope
43
+ end
44
+
45
+ def respond_to?(method, include_private = false)
46
+ return true if scopes.include?(method)
47
+ criteria.respond_to?(method, include_private)
48
+ end
49
+
50
+ private
51
+
52
+ def method_missing(method, *args, &block)
53
+ if scopes.include?(method)
54
+ scopes[method].call(self, *args)
55
+ else
56
+ chained = Mongoid::Criteria.new(klass)
57
+ chained.merge(criteria)
58
+ chained.send(method, *args, &block)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Contexts #:nodoc:
4
+ module Paging
5
+ # Paginates the documents.
6
+ #
7
+ # Example:
8
+ #
9
+ # <tt>context.paginate</tt>
10
+ #
11
+ # Returns:
12
+ #
13
+ # A collection of documents paginated.
14
+ def paginate
15
+ @collection ||= execute(true)
16
+ WillPaginate::Collection.create(page, per_page, count) do |pager|
17
+ pager.replace(@collection.to_a)
18
+ end
19
+ end
20
+
21
+ # Either returns the page option and removes it from the options, or
22
+ # returns a default value of 1.
23
+ #
24
+ # Returns:
25
+ #
26
+ # An +Integer+ page number.
27
+ def page
28
+ skips, limits = options[:skip], options[:limit]
29
+ (skips && limits) ? (skips + limits) / limits : 1
30
+ end
31
+
32
+ # Get the number of results per page or the default of 20.
33
+ #
34
+ # Returns:
35
+ #
36
+ # The +Integer+ number of documents in each page.
37
+ def per_page
38
+ (options[:limit] || 20).to_i
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,264 @@
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
+
7
+ module Mongoid #:nodoc:
8
+ # The +Criteria+ class is the core object needed in Mongoid to retrieve
9
+ # objects from the database. It is a DSL that essentially sets up the
10
+ # selector and options arguments that get passed on to a <tt>Mongo::Collection</tt>
11
+ # in the Ruby driver. Each method on the +Criteria+ returns self to they
12
+ # can be chained in order to create a readable criterion to be executed
13
+ # against the database.
14
+ #
15
+ # Example setup:
16
+ #
17
+ # <tt>criteria = Criteria.new</tt>
18
+ #
19
+ # <tt>criteria.only(:field).where(:field => "value").skip(20).limit(20)</tt>
20
+ #
21
+ # <tt>criteria.execute</tt>
22
+ class Criteria
23
+ include Criterion::Exclusion
24
+ include Criterion::Inclusion
25
+ include Criterion::Optional
26
+ include Enumerable
27
+
28
+ attr_reader :collection, :ids, :klass, :options, :selector
29
+
30
+ attr_accessor :documents
31
+
32
+ delegate \
33
+ :aggregate,
34
+ :count,
35
+ :execute,
36
+ :first,
37
+ :group,
38
+ :id_criteria,
39
+ :last,
40
+ :max,
41
+ :min,
42
+ :one,
43
+ :page,
44
+ :paginate,
45
+ :per_page,
46
+ :sum, :to => :context
47
+
48
+ # Concatinate the criteria with another enumerable. If the other is a
49
+ # +Criteria+ then it needs to get the collection from it.
50
+ def +(other)
51
+ entries + comparable(other)
52
+ end
53
+
54
+ # Returns the difference between the criteria and another enumerable. If
55
+ # the other is a +Criteria+ then it needs to get the collection from it.
56
+ def -(other)
57
+ entries - comparable(other)
58
+ end
59
+
60
+ # Returns true if the supplied +Enumerable+ or +Criteria+ is equal to the results
61
+ # of this +Criteria+ or the criteria itself.
62
+ #
63
+ # This will force a database load when called if an enumerable is passed.
64
+ #
65
+ # Options:
66
+ #
67
+ # other: The other +Enumerable+ or +Criteria+ to compare to.
68
+ def ==(other)
69
+ case other
70
+ when Criteria
71
+ self.selector == other.selector && self.options == other.options
72
+ when Enumerable
73
+ return (execute.entries == other)
74
+ else
75
+ return false
76
+ end
77
+ end
78
+
79
+ # Returns true if the criteria is empty.
80
+ #
81
+ # Example:
82
+ #
83
+ # <tt>criteria.blank?</tt>
84
+ def blank?
85
+ count < 1
86
+ end
87
+
88
+ alias :empty? :blank?
89
+
90
+ # Return or create the context in which this criteria should be executed.
91
+ #
92
+ # This will return an Enumerable context if the class is embedded,
93
+ # otherwise it will return a Mongo context for root classes.
94
+ def context
95
+ @context ||= Contexts.context_for(self)
96
+ end
97
+
98
+ # Iterate over each +Document+ in the results. This can take an optional
99
+ # block to pass to each argument in the results.
100
+ #
101
+ # Example:
102
+ #
103
+ # <tt>criteria.each { |doc| p doc }</tt>
104
+ def each(&block)
105
+ return caching(&block) if cached?
106
+ if block_given?
107
+ execute.each { |doc| yield doc }
108
+ end
109
+ self
110
+ end
111
+
112
+ # Merges the supplied argument hash into a single criteria
113
+ #
114
+ # Options:
115
+ #
116
+ # criteria_conditions: Hash of criteria keys, and parameter values
117
+ #
118
+ # Example:
119
+ #
120
+ # <tt>criteria.fuse(:where => { :field => "value"}, :limit => 20)</tt>
121
+ #
122
+ # Returns <tt>self</tt>
123
+ def fuse(criteria_conditions = {})
124
+ criteria_conditions.inject(self) do |criteria, (key, value)|
125
+ criteria.send(key, value)
126
+ end
127
+ end
128
+
129
+ # Create the new +Criteria+ object. This will initialize the selector
130
+ # and options hashes, as well as the type of criteria.
131
+ #
132
+ # Options:
133
+ #
134
+ # type: One of :all, :first:, or :last
135
+ # klass: The class to execute on.
136
+ def initialize(klass)
137
+ @selector, @options, @klass, @documents = {}, {}, klass, []
138
+ end
139
+
140
+ # Merges another object into this +Criteria+. The other object may be a
141
+ # +Criteria+ or a +Hash+. This is used to combine multiple scopes together,
142
+ # where a chained scope situation may be desired.
143
+ #
144
+ # Options:
145
+ #
146
+ # other: The +Criteria+ or +Hash+ to merge with.
147
+ #
148
+ # Example:
149
+ #
150
+ # <tt>criteria.merge({ :conditions => { :title => "Sir" } })</tt>
151
+ def merge(other)
152
+ @selector.update(other.selector)
153
+ @options.update(other.options)
154
+ @documents = other.documents
155
+ end
156
+
157
+ # Used for chaining +Criteria+ scopes together in the for of class methods
158
+ # on the +Document+ the criteria is for.
159
+ #
160
+ # Options:
161
+ #
162
+ # name: The name of the class method on the +Document+ to chain.
163
+ # args: The arguments passed to the method.
164
+ #
165
+ # Returns: <tt>Criteria</tt>
166
+ def method_missing(name, *args)
167
+ if @klass.respond_to?(name)
168
+ new_scope = @klass.send(name, *args)
169
+ new_scope.merge(self)
170
+ return new_scope
171
+ else
172
+ return entries.send(name, *args)
173
+ end
174
+ end
175
+
176
+ alias :to_ary :to_a
177
+
178
+ # Returns the selector and options as a +Hash+ that would be passed to a
179
+ # scope for use with named scopes.
180
+ def scoped
181
+ { :where => @selector }.merge(@options)
182
+ end
183
+
184
+ # Translate the supplied arguments into a +Criteria+ object.
185
+ #
186
+ # If the passed in args is a single +String+, then it will
187
+ # construct an id +Criteria+ from it.
188
+ #
189
+ # If the passed in args are a type and a hash, then it will construct
190
+ # the +Criteria+ with the proper selector, options, and type.
191
+ #
192
+ # Options:
193
+ #
194
+ # args: either a +String+ or a +Symbol+, +Hash combination.
195
+ #
196
+ # Example:
197
+ #
198
+ # <tt>Criteria.translate(Person, "4ab2bc4b8ad548971900005c")</tt>
199
+ # <tt>Criteria.translate(Person, :conditions => { :field => "value"}, :limit => 20)</tt>
200
+ def self.translate(*args)
201
+ klass = args[0]
202
+ params = args[1] || {}
203
+ unless params.is_a?(Hash)
204
+ return new(klass).id_criteria(params)
205
+ end
206
+ conditions = params.delete(:conditions) || {}
207
+ if conditions.include?(:id)
208
+ conditions[:_id] = conditions[:id]
209
+ conditions.delete(:id)
210
+ end
211
+ return new(klass).where(conditions).extras(params)
212
+ end
213
+
214
+ protected
215
+
216
+ # Iterate over each +Document+ in the results and cache the collection.
217
+ def caching(&block)
218
+ @collection ||= execute
219
+ if block_given?
220
+ docs = []
221
+ @collection.each do |doc|
222
+ docs << doc
223
+ yield doc
224
+ end
225
+ @collection = docs
226
+ end
227
+ self
228
+ end
229
+
230
+ # Filters the unused options out of the options +Hash+. Currently this
231
+ # takes into account the "page" and "per_page" options that would be passed
232
+ # in if using will_paginate.
233
+ #
234
+ # Example:
235
+ #
236
+ # Given a criteria with a selector of { :page => 1, :per_page => 40 }
237
+ #
238
+ # <tt>criteria.filter_options</tt> # selector: { :skip => 0, :limit => 40 }
239
+ def filter_options
240
+ page_num = @options.delete(:page)
241
+ per_page_num = @options.delete(:per_page)
242
+ if (page_num || per_page_num)
243
+ @options[:limit] = limits = (per_page_num || 20).to_i
244
+ @options[:skip] = (page_num || 1).to_i * limits - limits
245
+ end
246
+ end
247
+
248
+ # Return the entries of the other criteria or the object. Used for
249
+ # comparing criteria or an enumerable.
250
+ def comparable(other)
251
+ other.is_a?(Criteria) ? other.entries : other
252
+ end
253
+
254
+ # Update the selector setting the operator on the value for each key in the
255
+ # supplied attributes +Hash+.
256
+ #
257
+ # Example:
258
+ #
259
+ # <tt>criteria.update_selector({ :field => "value" }, "$in")</tt>
260
+ def update_selector(attributes, operator)
261
+ attributes.each { |key, value| @selector[key] = { operator => value } }; self
262
+ end
263
+ end
264
+ end