lib/mongodoc/criteria.rb in mongodoc-0.1.2 vs lib/mongodoc/criteria.rb in mongodoc-0.2.0
- old
+ new
@@ -10,11 +10,11 @@
#
# Example setup:
#
# <tt>criteria = Criteria.new</tt>
#
- # <tt>criteria.select(:field => "value").only(:field).skip(20).limit(20)</tt>
+ # <tt>criteria.only(:field => "value").only(:field).skip(20).limit(20)</tt>
#
# <tt>criteria.execute</tt>
class Criteria
SORT_REVERSALS = {
:asc => :desc,
@@ -23,12 +23,22 @@
:descending => :ascending
}
include Enumerable
- attr_reader :klass, :options, :selector
+ attr_reader :collection, :klass, :options, :selector
+ # Create the new +Criteria+ object. This will initialize the selector
+ # and options hashes, as well as the type of criteria.
+ #
+ # Options:
+ #
+ # klass: The class to execute on.
+ def initialize(klass)
+ @selector, @options, @klass = {}, {}, klass
+ end
+
# Returns true if the supplied +Enumerable+ or +Criteria+ is equal to the results
# of this +Criteria+ or the criteria itself.
#
# This will force a database load when called if an enumerable is passed.
#
@@ -39,11 +49,11 @@
case other
when Criteria
self.selector == other.selector && self.options == other.options
when Enumerable
@collection ||= execute
- return (@collection == other)
+ return (collection == other)
else
return false
end
end
@@ -53,35 +63,24 @@
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with counts.
#
# Example:
#
- # <tt>criteria.select(:field1).where(:field1 => "Title").aggregate(Person)</tt>
- def aggregate(use_klass = nil)
- aggregating_klass = use_klass ? use_klass : klass
- aggregating_klass.collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE)
+ # <tt>criteria.only(:field1).where(:field1 => "Title").aggregate</tt>
+ def aggregate
+ klass.collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE, true)
end
- # Adds a criterion to the +Criteria+ that specifies values that must all
- # be matched in order to return results. Similar to an "in" clause but the
- # underlying conditional logic is an "AND" and not an "OR". The MongoDB
- # conditional operator that will be used is "$all".
+ # Get all the matching documents in the database for the +Criteria+.
#
- # Options:
- #
- # selections: A +Hash+ where the key is the field name and the value is an
- # +Array+ of values that must all match.
- #
# Example:
#
- # <tt>criteria.every(:field => ["value1", "value2"])</tt>
+ # <tt>criteria.all</tt>
#
- # <tt>criteria.every(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
- #
- # Returns: <tt>self</tt>
- def every(selections = {})
- selections.each { |key, value| selector[key] = { "$all" => value } }; self
+ # Returns: <tt>Array</tt>
+ def all
+ collect
end
# Get the count of matching documents in the database for the +Criteria+.
#
# Example:
@@ -100,16 +99,101 @@
#
# <tt>criteria.each { |doc| p doc }</tt>
def each(&block)
@collection ||= execute
if block_given?
- @collection.each(&block)
- else
- self
+ @collection = collection.inject([]) do |container, item|
+ container << item
+ yield item
+ container
+ end
end
+ self
end
+ GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
+ # Groups the criteria. This will take the internally built selector and options
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
+ # collection itself will be retrieved from the class provided, and once the
+ # query has returned it will provided a grouping of keys with objects.
+ #
+ # Example:
+ #
+ # <tt>criteria.only(:field1).where(:field1 => "Title").group</tt>
+ def group
+ klass.collection.group(
+ options[:fields],
+ selector,
+ { :group => [] },
+ GROUP_REDUCE,
+ true
+ ).collect {|docs| docs["group"] = MongoDoc::BSON.decode(docs["group"]); docs }
+ end
+
+ # Return the last result for the +Criteria+. Essentially does a find_one on
+ # the collection with the sorting reversed. If no sorting parameters have
+ # been provided it will default to ids.
+ #
+ # Example:
+ #
+ # <tt>Criteria.only(:name).where(:name = "Chrissy").last</tt>
+ def last
+ opts = options.dup
+ sorting = opts[:sort]
+ sorting = [[:_id, :asc]] unless sorting
+ opts[:sort] = sorting.collect { |option| [ option.first, Criteria.invert(option.last) ] }
+ klass.collection.find_one(selector, opts)
+ end
+
+ # Return the first result for the +Criteria+.
+ #
+ # Example:
+ #
+ # <tt>Criteria.only(:name).where(:name = "Chrissy").one</tt>
+ def one
+ klass.collection.find_one(selector, options.dup)
+ end
+ alias :first :one
+
+ # Translate the supplied argument hash
+ #
+ # Options:
+ #
+ # criteria_conditions: Hash of criteria keys, and parameter values
+ #
+ # Example:
+ #
+ # <tt>criteria.criteria(:where => { :field => "value"}, :limit => 20)</tt>
+ #
+ # Returns <tt>self</tt>
+ def criteria(criteria_conditions = {})
+ criteria_conditions.inject(self) do |criteria, (key, value)|
+ criteria.send(key, value)
+ end
+ end
+
+ # Adds a criterion to the +Criteria+ that specifies values that must all
+ # be matched in order to return results. Similar to an "in" clause but the
+ # underlying conditional logic is an "AND" and not an "OR". The MongoDB
+ # conditional operator that will be used is "$all".
+ #
+ # Options:
+ #
+ # selections: A +Hash+ where the key is the field name and the value is an
+ # +Array+ of values that must all match.
+ #
+ # Example:
+ #
+ # <tt>criteria.every(:field => ["value1", "value2"])</tt>
+ #
+ # <tt>criteria.every(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
+ #
+ # Returns: <tt>self</tt>
+ def every(selections = {})
+ selections.each { |key, value| selector[key] = { "$all" => value } }; self
+ end
+
# Adds a criterion to the +Criteria+ that specifies values that are not allowed
# to match any document in the database. The MongoDB conditional operator that
# will be used is "$ne".
#
# Options:
@@ -144,36 +228,23 @@
options.merge!(extras)
filter_options
self
end
- # Return the first result for the +Criteria+.
+ # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
#
- # Example:
+ # Options:
#
- # <tt>Criteria.select(:name).where(:name = "Chrissy").one</tt>
- def one
- klass.collection.find_one(selector, options.dup)
- end
- alias :first :one
-
- GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
- # Groups the criteria. This will take the internally built selector and options
- # and pass them on to the Ruby driver's +group()+ method on the collection. The
- # collection itself will be retrieved from the class provided, and once the
- # query has returned it will provided a grouping of keys with objects.
+ # id_or_object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
#
# Example:
#
- # <tt>criteria.select(:field1).where(:field1 => "Title").group(Person)</tt>
- def group(use_klass = nil)
- (use_klass || klass).collection.group(
- options[:fields],
- selector,
- { :group => [] },
- GROUP_REDUCE
- ).collect {|docs| docs["group"] = MongoDoc::BSON.decode(docs["group"]); docs }
+ # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
+ #
+ # Returns: <tt>self</tt>
+ def id(id_or_object_id)
+ selector[:_id] = id_or_object_id; self
end
# Adds a criterion to the +Criteria+ that specifies values where any can
# be matched in order to return results. This is similar to an SQL "IN"
# clause. The MongoDB conditional operator that will be used is "$in".
@@ -192,51 +263,10 @@
# Returns: <tt>self</tt>
def in(inclusions = {})
inclusions.each { |key, value| selector[key] = { "$in" => value } }; self
end
- # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
- #
- # Options:
- #
- # object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
- #
- # Example:
- #
- # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
- #
- # Returns: <tt>self</tt>
- def id(object_id)
- selector[:_id] = object_id; self
- end
-
- # Create the new +Criteria+ object. This will initialize the selector
- # and options hashes, as well as the type of criteria.
- #
- # Options:
- #
- # type: One of :all, :first:, or :last
- # klass: The class to execute on.
- def initialize(klass)
- @selector, @options, @klass = {}, {}, klass
- end
-
- # Return the last result for the +Criteria+. Essentially does a find_one on
- # the collection with the sorting reversed. If no sorting parameters have
- # been provided it will default to ids.
- #
- # Example:
- #
- # <tt>Criteria.select(:name).where(:name = "Chrissy").last</tt>
- def last
- opts = options.dup
- sorting = opts[:sort]
- sorting = [[:_id, :asc]] unless sorting
- opts[:sort] = sorting.collect { |option| [ option.first, Criteria.invert(option.last) ] }
- klass.collection.find_one(selector, opts)
- end
-
# Adds a criterion to the +Criteria+ that specifies the maximum number of
# results to return. This is mostly used in conjunction with <tt>skip()</tt>
# to handle paginated results.
#
# Options:
@@ -250,60 +280,10 @@
# Returns: <tt>self</tt>
def limit(value = 20)
options[:limit] = value; self
end
- # Merges another object into this +Criteria+. The other object may be a
- # +Criteria+ or a +Hash+. This is used to combine multiple scopes together,
- # where a chained scope situation may be desired.
- #
- # Options:
- #
- # other: The +Criteria+ or +Hash+ to merge with.
- #
- # Example:
- #
- # <tt>criteria.merge({ :conditions => { :title => "Sir" } })</tt>
- def merge(other)
- selector.update(other.selector)
- options.update(other.options)
- end
-
- # Used for chaining +Criteria+ scopes together in the for of class methods
- # on the +Document+ the criteria is for.
- #
- # Options:
- #
- # name: The name of the class method on the +Document+ to chain.
- # args: The arguments passed to the method.
- #
- # Example:
- #
- # class Person < Mongoid::Document
- # field :title
- # field :terms, :type => Boolean, :default => false
- #
- # class << self
- # def knights
- # all(:conditions => { :title => "Sir" })
- # end
- #
- # def accepted
- # all(:conditions => { :terms => true })
- # end
- # end
- # end
- #
- # Person.accepted.knights #returns a merged criteria of the 2 scopes.
- #
- # Returns: <tt>Criteria</tt>
- def method_missing(name, *args)
- new_scope = klass.send(name)
- new_scope.merge(self)
- new_scope
- end
-
# Adds a criterion to the +Criteria+ that specifies values where none
# should match in order to return results. This is similar to an SQL "NOT IN"
# clause. The MongoDB conditional operator that will be used is "$nin".
#
# Options:
@@ -361,11 +341,11 @@
#
# <tt>criteria.paginate</tt>
def paginate
@collection ||= execute
WillPaginate::Collection.create(page, per_page, count) do |pager|
- pager.replace(@collection.to_a)
+ pager.replace(collection.to_a)
end
end
# Returns the number of results per page or the default of 20.
def per_page
@@ -380,14 +360,14 @@
#
# args: A list of field names to retrict the returned fields to.
#
# Example:
#
- # <tt>criteria.select(:field1, :field2, :field3)</tt>
+ # <tt>criteria.only(:field1, :field2, :field3)</tt>
#
# Returns: <tt>self</tt>
- def select(*args)
+ def only(*args)
options[:fields] = args.flatten if args.any?; self
end
# Adds a criterion to the +Criteria+ that specifies how many results to skip
# when returning Documents. This is mostly used in conjunction with
@@ -405,10 +385,40 @@
# Returns: <tt>self</tt>
def skip(value = 0)
options[:skip] = value; self
end
+ # Adds a criterion to the +Criteria+ that specifies values that must
+ # be matched in order to return results. This is similar to a SQL "WHERE"
+ # clause. This is the actual selector that will be provided to MongoDB,
+ # similar to the Javascript object that is used when performing a find()
+ # in the MongoDB console.
+ #
+ # Options:
+ #
+ # selector_or_js: A +Hash+ that must match the attributes of the +Document+
+ # or a +String+ of js code.
+ #
+ # Example:
+ #
+ # <tt>criteria.where(:field1 => "value1", :field2 => 15)</tt>
+ #
+ # <tt>criteria.where('this.a > 3')</tt>
+ #
+ # Returns: <tt>self</tt>
+ def where(selector_or_js = {})
+ case selector_or_js
+ when String
+ selector['$where'] = selector_or_js
+ else
+ selector.merge!(selector_or_js)
+ end
+ self
+ end
+ alias :and :where
+ alias :conditions :where
+
# Translate the supplied arguments into a +Criteria+ object.
#
# If the passed in args is a single +String+, then it will
# construct an id +Criteria+ from it.
#
@@ -425,30 +435,11 @@
#
# <tt>Criteria.translate(Person, :conditions => { :field => "value"}, :limit => 20)</tt>
#
# Returns a new +Criteria+ object.
def self.translate(klass, params = {})
- return new(klass).id(params).one if params.is_a?(String)
- return new(klass).where(params.delete(:conditions)).extras(params)
- end
-
- # Adds a criterion to the +Criteria+ that specifies values that must
- # be matched in order to return results. This is similar to a SQL "WHERE"
- # clause. This is the actual selector that will be provided to MongoDB,
- # similar to the Javascript object that is used when performing a find()
- # in the MongoDB console.
- #
- # Options:
- #
- # selectior: A +Hash+ that must match the attributes of the +Document+.
- #
- # Example:
- #
- # <tt>criteria.where(:field1 => "value1", :field2 => 15)</tt>
- #
- # Returns: <tt>self</tt>
- def where(add_selector = {})
- selector.merge!(add_selector); self
+ return new(klass).id(params).one unless params.is_a?(Hash)
+ return new(klass).criteria(params)
end
protected
# Execute the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +find()+ method on the collection. The