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