mongodoc 0.2.2 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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
data/Rakefile CHANGED
@@ -81,6 +81,27 @@ namespace :mongo do
81
81
  end
82
82
  end
83
83
 
84
+ namespace :mongoid do
85
+ desc 'Sync criteria from Mongoid'
86
+ task :sync do
87
+ require 'pathname'
88
+
89
+ src_dir = Pathname.new('../durran-mongoid/lib/mongoid')
90
+ dest_dir = Pathname.new('lib/mongoid')
91
+ dest_dir.mkpath
92
+ %w(criteria.rb contexts/paging.rb criterion extensions/symbol/inflections.rb extensions/hash/criteria_helpers.rb matchers).each do |f|
93
+ src = src_dir + f
94
+ if src.directory?
95
+ FileUtils.cp_r(src, dest_dir)
96
+ else
97
+ dest = dest_dir + f
98
+ dest.dirname.mkpath
99
+ FileUtils.cp(src, dest)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
84
105
  namespace :bench do
85
106
  desc 'Run benchmark for MongoDoc'
86
107
  task 'mongodoc' do
data/TODO CHANGED
@@ -1,10 +1,15 @@
1
- As of 2010-02-05
1
+ As of 2010-02-23
2
2
 
3
3
  Associations
4
4
  ------------
5
5
 
6
6
  Pull associations out of attributes and refactor
7
7
 
8
+ Criteria
9
+ --------
10
+
11
+ Make critieria module, include into Document, CriteriaProxy, Association?
12
+
8
13
  Documentation
9
14
  -------------
10
15
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.2.4
@@ -52,7 +52,7 @@ Feature: Finders
52
52
  And I save the document 'contractor'
53
53
 
54
54
  Scenario: All
55
- When I query contacts with all
55
+ When I query contacts with find_all
56
56
  Then the query result has 3 documents
57
57
 
58
58
  Scenario: Count
@@ -109,11 +109,20 @@ Feature: MongoDoc::Base
109
109
  Then the Place collection should have 1 document
110
110
  And the document 'hashrocket' roundtrips
111
111
 
112
- Scenario: Finder
112
+ Scenario: Class criteria
113
113
  Given an empty Contact document collection
114
114
  And a Contact document named 'hashrocket' :
115
115
  | Name | Type |
116
116
  | Hashrocket | company |
117
117
  And I save the last document
118
- When I query contacts with find {:where => {'type' => 'company'}}
118
+ When I query contacts with criteria where('type' => 'company')
119
+ Then the size of the last return value is 1
120
+
121
+ Scenario: Finder
122
+ Given an empty Contact document collection
123
+ And a Contact document named 'hashrocket' :
124
+ | Name | Type |
125
+ | Hashrocket | company |
126
+ And I save the document 'hashrocket'
127
+ When I find a contact using the id of 'hashrocket'
119
128
  Then the size of the last return value is 1
@@ -42,7 +42,7 @@ end
42
42
 
43
43
  Given /^I set the id on the document '(.*)' to (.*)$/ do |doc_name, value|
44
44
  doc = instance_variable_get("@#{doc_name}")
45
- doc._id = Mongo::ObjectID.new([value.to_i])
45
+ doc._id = Mongo::ObjectID.from_string("%024x" % value.to_i(16))
46
46
  end
47
47
 
48
48
  Given /^'(.+)' has one (.+?) as (.+?) \(identified by '(.+)'\):$/ do |doc_name, class_name, assoc_name, var_name, table|
@@ -79,10 +79,21 @@ When /^I update the document '(.*)' with the hash named '(.*)'$/ do |doc_name, h
79
79
  @last_return = doc.update_attributes(attrs)
80
80
  end
81
81
 
82
- When /^I query (.*) with find (.*)$/ do |doc, criteria_text|
82
+ When /^I query (.*) with criteria (.*)$/ do |doc, criteria_text|
83
+ klass = doc.singularize.camelize
84
+ @query = @last_return = eval("#{klass}.criteria.#{criteria_text}")
85
+ end
86
+
87
+ When /^I query (.*) with the '(.*)' id$/ do |doc, name|
83
88
  klass = doc.singularize.camelize.constantize
84
- criteria = eval(criteria_text)
85
- @last_return = klass.find(:all, criteria)
89
+ doc = instance_variable_get("@#{name}")
90
+ @query = @last_return = klass.criteria.id(doc.id).entries
91
+ end
92
+
93
+ When /^I find a (.*) using the id of '(.*)'$/ do |type, doc_name|
94
+ klass = type.camelize.constantize
95
+ doc = instance_variable_get("@#{doc_name}")
96
+ @last_return = klass.find(doc.id)
86
97
  end
87
98
 
88
99
  When /^'(.+)' is the first (.+?) of '(.+)'$/ do |var_name, single_assoc, doc_name|
@@ -24,7 +24,7 @@ class Contact
24
24
  key :interests
25
25
  has_many :addresses
26
26
 
27
- named_scope :rubyists, :in => {:interests => ['ruby']}
28
- named_scope :contract_work, :in => {:interests => ['contract work']}
29
- named_scope :in_state, lambda {|state| { :where => {'addresses.state' => state}}}
27
+ scope :rubyists, any_in(:interests => ['ruby'])
28
+ scope :contract_work, any_in(:interests => ['contract work'])
29
+ scope :in_state, lambda {|state| where('addresses.state' => state)}
30
30
  end
@@ -6,19 +6,23 @@ def query(klass_name = nil)
6
6
  @query ||= klass(klass_name).criteria
7
7
  end
8
8
 
9
- Then /^the (first|last) query result is equal to the document '(.*)'$/ do |position, name|
10
- object = instance_variable_get("@#{name}")
11
- query.send(position).should == object
9
+ Then /^the query result is equal to the document '(.*)'$/ do |name|
10
+ doc = instance_variable_get("@#{name}")
11
+ query.should == doc
12
12
  end
13
13
 
14
14
  Then /^one of the query results is the document '(.*)'$/ do |name|
15
- object = instance_variable_get("@#{name}")
16
- query.any? {|doc| doc == object}
15
+ doc = instance_variable_get("@#{name}")
16
+ query.any? {|d| d == doc}.should be_true
17
17
  end
18
18
 
19
- Then /^the aggregate query result with "(.*)" == "(.*)" has a count of (.*)$/ do |key, value, count|
20
- result = query.aggregate
21
- result.find {|r| r.has_key?(key) and r[key] == value }['count'].should == count.to_i
19
+ Then /^the query result with "(.*)" == "(.*)" has a count of (.*)$/ do |key, value, count|
20
+ query.find {|r| r.has_key?(key) and r[key] == value }['count'].should == count.to_i
21
+ end
22
+
23
+ Then /^the query result with "([^\"]*)" == "([^\"]*)" has the document '(.*)'$/ do |key, value, name|
24
+ doc = instance_variable_get("@#{name}")
25
+ query.find {|r| r.has_key?(key) and r[key] == value }['group'].should include(doc)
22
26
  end
23
27
 
24
28
  Then /^the query result has (.*) documents*$/ do |count|
@@ -29,14 +33,13 @@ Then /^the query result has (.*) documents*$/ do |count|
29
33
  end
30
34
  end
31
35
 
32
- Then /^the size of the query result is (.*)$/ do |count|
33
- query.to_a.size.should == count.to_i
36
+ Then /^the (first|last) query result is the document '(.*)'$/ do |position, name|
37
+ doc = instance_variable_get("@#{name}")
38
+ query.entries.send(position).should == doc
34
39
  end
35
40
 
36
- Then /^the group query result with "([^\"]*)" == "([^\"]*)" has the document '(.*)'$/ do |key, value, name|
37
- object = instance_variable_get("@#{name}")
38
- result = query.group
39
- result.find {|r| r.has_key?(key) and r[key] == value }['group'].should include(object)
41
+ Then /^the size of the query result is (.*)$/ do |count|
42
+ query.to_a.size.should == count.to_i
40
43
  end
41
44
 
42
45
  Then /^the query result is the document '(.*)'$/ do |name|
@@ -74,76 +74,55 @@ Feature: MongoDoc::Base
74
74
  And I save the document 'jax'
75
75
 
76
76
  Scenario: Counting results
77
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
77
+ When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment'])
78
78
  Then the query result has 2 documents
79
79
 
80
80
  Scenario: Finding contacts with interests in ruby and rails
81
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
82
- Then the query result has 2 documents
83
- And one of the query results is the document 'rocketeer'
81
+ When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment'])
82
+ Then one of the query results is the document 'rocketeer'
84
83
 
85
- Scenario: Finding contacts with interests in restaurants and hotels
86
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
87
- Then the query result has 2 documents
88
- And one of the query results is the document 'contractor'
84
+ Scenario: Finding contacts with interests in restaurants or hotels
85
+ When I query contacts with criteria in('interests' => ['restaurants', 'hotels'])
86
+ Then one of the query results is the document 'contractor'
89
87
 
90
88
  Scenario: Aggregating Places
91
- When I query places to select field "type"
92
- And I query places where "{'address.state' => 'FL'}"
93
- Then the aggregate query result with "type" == "hotel" has a count of 2
89
+ When I query places with criteria only('type').where('address.state' => 'FL').aggregate
90
+ Then the query result with "type" == "hotel" has a count of 2
94
91
 
95
92
  Scenario: Excluding places in Neptune Beach
96
- When I query places to select field "type"
97
- And I query places that excludes "{'address.city' => 'Neptune Beach'}"
98
- Then the aggregate query result with "type" == "hotel" has a count of 1
93
+ When I query places with criteria only('type').where('address.city' => 'Neptune Beach').aggregate
94
+ Then the query result with "type" == "hotel" has a count of 1
99
95
 
100
96
  Scenario: Using extras to limit results
101
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
102
- And I set the query extras limit on contacts to 1
97
+ When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment']).limit(1)
103
98
  Then the size of the query result is 1
104
99
 
105
100
  Scenario: Finding the first result
106
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
107
- Then the first query result is equal to the document 'hashrocket'
101
+ When I query contacts with criteria all('interests' => ['ruby', 'rails', 'employment']).first
102
+ Then the query result is equal to the document 'hashrocket'
108
103
 
109
104
  Scenario: Grouping places by type
110
- When I query places to select field "type"
111
- And I query places where "{'type' => 'hotel'}"
112
- Then the group query result with "type" == "hotel" has the document 'one_ocean'
105
+ When I query places with criteria only('type').where('type' => 'hotel').group
106
+ Then the query result with "type" == "hotel" has the document 'one_ocean'
113
107
 
114
108
  Scenario: Selecting contacts with in operator
115
- When I query contacts with in "{'interests' => ['ruby', 'rails', 'employment']}"
109
+ When I query contacts with criteria in('interests' => ['ruby', 'rails', 'employment'])
116
110
  Then the query result has 3 documents
117
111
 
118
112
  Scenario: Selecting a contact with the id operator
119
113
  When I query contacts with the 'hashrocket' id
120
114
  Then the query result has 1 documents
121
- And the first query result is equal to the document 'hashrocket'
122
-
123
- Scenario: Finding the last result
124
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
125
- Then the last query result is equal to the document 'rocketeer'
126
-
127
- Scenario: Using limit on results
128
- When I query contacts with every "{'interests' => ['ruby', 'rails', 'employment']}"
129
- And I set the query on contacts to limit 1
130
- Then the size of the query result is 1
115
+ And the query result is the document 'hashrocket'
131
116
 
132
117
  Scenario: Selecting contacts with not in operator
133
- When I query contacts with not in "{'interests' => ['contract work', 'employment']}"
118
+ When I query contacts with criteria not_in('interests' => ['contract work', 'employment'])
134
119
  Then the query result has 0 documents
135
120
 
136
121
  Scenario: Ordering contacts
137
- When I query contacts with in "{'interests' => ['ruby', 'rails']}"
138
- And I order the contacts query by "[[:name, :asc]]"
139
- Then the first query result is equal to the document 'contractor'
140
- Then the last query result is equal to the document 'rocketeer'
122
+ When I query contacts with criteria in('interests' => ['ruby', 'rails']).order_by([[:name, :asc]]).entries
123
+ Then the first query result is the document 'contractor'
124
+ And the last query result is the document 'rocketeer'
141
125
 
142
126
  Scenario: Using skip on results
143
- When I query contacts with every "{'interests' => ['ruby', 'rails']}"
144
- And I set the query on contacts to skip 1
127
+ When I query contacts with criteria all('interests' => ['ruby', 'rails']).skip(1)
145
128
  Then the size of the query result is 2
146
-
147
- Scenario: All
148
- When I query contacts with 'all'
149
- Then the size of the query result is 3
@@ -11,7 +11,7 @@ require 'validatable'
11
11
  require 'will_paginate/collection'
12
12
 
13
13
  module MongoDoc
14
- VERSION = '0.2.2'
14
+ VERSION = '0.2.4'
15
15
  end
16
16
 
17
17
  require 'mongodoc/connection'
@@ -2,7 +2,9 @@
2
2
  # http://github.com/sandro
3
3
  module MongoDoc
4
4
  module Associations
5
- class CollectionProxy < ProxyBase
5
+ class CollectionProxy
6
+ include ProxyBase
7
+
6
8
  # List of array methods (that are not in +Object+) that need to be
7
9
  # delegated to +collection+.
8
10
  ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
@@ -1,9 +1,12 @@
1
1
  module MongoDoc
2
2
  module Associations
3
- class DocumentProxy < ProxyBase
3
+ class DocumentProxy
4
+ include ProxyBase
4
5
 
5
6
  attr_reader :document
6
7
 
8
+ delegate :to_bson, :id, :to => :document
9
+
7
10
  def _root=(root)
8
11
  @_root = root
9
12
  document._root = root if is_document?(document)
@@ -2,7 +2,9 @@ module MongoDoc
2
2
  class InvalidEmbeddedHashKey < RuntimeError; end
3
3
 
4
4
  module Associations
5
- class HashProxy < ProxyBase
5
+ class HashProxy
6
+ include ProxyBase
7
+
6
8
  HASH_METHODS = (Hash.instance_methods - Object.instance_methods).map { |n| n.to_s }
7
9
 
8
10
  MUST_DEFINE = %w[to_a inspect to_bson ==]
@@ -1,9 +1,11 @@
1
1
  module MongoDoc
2
2
  module Associations
3
- class ProxyBase
4
- undef_method :id, :to_bson
5
-
6
- attr_reader :assoc_name, :assoc_class, :_parent, :_root
3
+ module ProxyBase
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ attr_reader :assoc_name, :assoc_class, :_parent, :_root
7
+ end
8
+ end
7
9
 
8
10
  def _parent=(parent)
9
11
  @_parent = parent
@@ -51,7 +51,7 @@ module MongoDoc
51
51
  def has_one(*args)
52
52
  options = args.extract_options!
53
53
  assoc_class = if class_name = options.delete(:class_name)
54
- type_name_with_module(class_name).constantize
54
+ self.class_from_name(class_name)
55
55
  end
56
56
 
57
57
  args.each do |name|
@@ -75,7 +75,7 @@ module MongoDoc
75
75
  def has_many(*args)
76
76
  options = args.extract_options!
77
77
  assoc_class = if class_name = options.delete(:class_name)
78
- type_name_with_module(class_name).constantize
78
+ self.class_from_name(class_name)
79
79
  end
80
80
 
81
81
  args.each do |name|
@@ -84,7 +84,7 @@ module MongoDoc
84
84
  define_method("#{name}") do
85
85
  association = instance_variable_get("@#{name}")
86
86
  unless association
87
- association = Associations::CollectionProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.type_name_with_module(name.to_s.classify).constantize)
87
+ association = Associations::CollectionProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.class_from_name(name))
88
88
  instance_variable_set("@#{name}", association)
89
89
  end
90
90
  association
@@ -105,7 +105,7 @@ module MongoDoc
105
105
  def has_hash(*args)
106
106
  options = args.extract_options!
107
107
  assoc_class = if class_name = options.delete(:class_name)
108
- type_name_with_module(class_name).constantize
108
+ self.class_from_name(class_name)
109
109
  end
110
110
 
111
111
  args.each do |name|
@@ -114,7 +114,7 @@ module MongoDoc
114
114
  define_method("#{name}") do
115
115
  association = instance_variable_get("@#{name}")
116
116
  unless association
117
- association = Associations::HashProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.type_name_with_module(name.to_s.classify).constantize)
117
+ association = Associations::HashProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.class_from_name(name))
118
118
  instance_variable_set("@#{name}", association)
119
119
  end
120
120
  association
@@ -133,7 +133,7 @@ module MongoDoc
133
133
  end
134
134
 
135
135
  def type_name_with_module(type_name)
136
- (/^::/ =~ type_name) ? type_name : "#{parents}::#{type_name}"
136
+ (/^::/ =~ type_name) ? type_name : "#{parent}::#{type_name}"
137
137
  end
138
138
  end
139
139
  end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ require "mongoid/contexts/paging"
3
+ require "mongodoc/contexts/enumerable"
4
+ require "mongodoc/contexts/mongo"
5
+
6
+ module Mongoid
7
+ module Contexts
8
+ # Determines the context to be used for this criteria. If the class is an
9
+ # embedded document, then the context will be the array in the has_many
10
+ # association it is in. If the class is a root, then the database itself
11
+ # will be the context.
12
+ #
13
+ # Example:
14
+ #
15
+ # <tt>Contexts.context_for(criteria)</tt>
16
+ def self.context_for(criteria)
17
+ if criteria.klass.respond_to?(:collection)
18
+ return MongoDoc::Contexts::Mongo.new(criteria)
19
+ end
20
+ return MongoDoc::Contexts::Enumerable.new(criteria)
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+ module MongoDoc #:nodoc:
3
+ module Contexts #:nodoc:
4
+ class Enumerable
5
+ include Mongoid::Contexts::Paging
6
+ attr_reader :criteria
7
+
8
+ delegate :first, :last, :to => :execute
9
+ delegate :documents, :options, :selector, :to => :criteria
10
+
11
+ # Return aggregation counts of the grouped documents. This will count by
12
+ # the first field provided in the fields array.
13
+ #
14
+ # Returns:
15
+ #
16
+ # A +Hash+ with field values as keys, count as values
17
+ def aggregate
18
+ counts = {}
19
+ group.each_pair { |key, value| counts[key] = value.size }
20
+ counts
21
+ end
22
+
23
+ # Gets the number of documents in the array. Delegates to size.
24
+ def count
25
+ @count ||= documents.size
26
+ end
27
+
28
+ # Groups the documents by the first field supplied in the field options.
29
+ #
30
+ # Returns:
31
+ #
32
+ # A +Hash+ with field values as keys, arrays of documents as values.
33
+ def group
34
+ field = options[:fields].first
35
+ documents.group_by { |doc| doc.send(field) }
36
+ end
37
+
38
+ # Enumerable implementation of execute. Returns matching documents for
39
+ # the selector, and adds options if supplied.
40
+ #
41
+ # Returns:
42
+ #
43
+ # An +Array+ of documents that matched the selector.
44
+ def execute(paginating = false)
45
+ limit(documents.select { |document| document.matches?(selector) })
46
+ end
47
+
48
+ # Return documents based on an id search. Will handle if a single id has
49
+ # been passed or mulitple ids.
50
+ #
51
+ # Example:
52
+ #
53
+ # context.id_criteria([1, 2, 3])
54
+ #
55
+ # Returns:
56
+ #
57
+ # The single or multiple documents.
58
+ def id_criteria(params)
59
+ criteria.id(params)
60
+ result = params.is_a?(String) ? one : criteria.entries
61
+ return result
62
+ end
63
+
64
+ # Create the new enumerable context. This will need the selector and
65
+ # options from a +Criteria+ and a documents array that is the underlying
66
+ # array of embedded documents from a has many association.
67
+ #
68
+ # Example:
69
+ #
70
+ # <tt>MongoDoc::Contexts::Enumerable.new(criteria)</tt>
71
+ def initialize(criteria)
72
+ @criteria = criteria
73
+ end
74
+
75
+ # Get the largest value for the field in all the documents.
76
+ #
77
+ # Returns:
78
+ #
79
+ # The numerical largest value.
80
+ def max(field)
81
+ determine(field, :>=)
82
+ end
83
+
84
+ # Get the smallest value for the field in all the documents.
85
+ #
86
+ # Returns:
87
+ #
88
+ # The numerical smallest value.
89
+ def min(field)
90
+ determine(field, :<=)
91
+ end
92
+
93
+ # Get one document.
94
+ #
95
+ # Returns:
96
+ #
97
+ # The first document in the +Array+
98
+ alias :one :first
99
+
100
+ # Get the sum of the field values for all the documents.
101
+ #
102
+ # Returns:
103
+ #
104
+ # The numerical sum of all the document field values.
105
+ def sum(field)
106
+ sum = documents.inject(nil) do |memo, doc|
107
+ value = doc.send(field)
108
+ memo ? memo += value : value
109
+ end
110
+ end
111
+
112
+ protected
113
+ # If the field exists, perform the comparison and set if true.
114
+ def determine(field, operator)
115
+ matching = documents.inject(nil) do |memo, doc|
116
+ value = doc.send(field)
117
+ (memo && memo.send(operator, value)) ? memo : value
118
+ end
119
+ end
120
+
121
+ # Limits the result set if skip and limit options.
122
+ def limit(documents)
123
+ skip, limit = options[:skip], options[:limit]
124
+ if skip && limit
125
+ return documents.slice(skip, limit)
126
+ end
127
+ documents
128
+ end
129
+ end
130
+ end
131
+ end
132
+