lib/mongodoc/criteria.rb in mongodoc-0.2.2 vs lib/mongodoc/criteria.rb in mongodoc-0.2.4

- old
+ new

@@ -1,481 +1,38 @@ -# This awesomeness is taken from Mongoid, Thanks Durran! +require 'mongoid/extensions/hash/criteria_helpers' +require 'mongoid/extensions/symbol/inflections' +require 'mongodoc/matchers' +require 'mongodoc/contexts' +require 'mongoid/criteria' -module MongoDoc #:nodoc: - # The +Criteria+ class is the core object needed in Mongoid to retrieve - # objects from the database. It is a DSL that essentially sets up the - # selector and options arguments that get passed on to a <tt>Mongo::Collection</tt> - # in the Ruby driver. Each method on the +Criteria+ returns self to they - # can be chained in order to create a readable criterion to be executed - # against the database. - # - # Example setup: - # - # <tt>criteria = Criteria.new</tt> - # - # <tt>criteria.only(:field => "value").only(:field).skip(20).limit(20)</tt> - # - # <tt>criteria.execute</tt> - class Criteria - SORT_REVERSALS = { - :asc => :desc, - :ascending => :descending, - :desc => :asc, - :descending => :ascending - } - - include Enumerable - - 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. +module MongoDoc + module Criteria + # Create a criteria for this +Document+ class # - # Options: - # - # klass: The class to execute on. - def initialize(klass) - @selector, @options, @klass = {}, {}, klass + # <tt>Person.criteria</tt> + def criteria + Mongoid::Criteria.new(self) 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. - # - # Options: - # - # other: The other +Enumerable+ or +Criteria+ to compare to. - def ==(other) - case other - when Criteria - self.selector == other.selector && self.options == other.options - when Enumerable - @collection ||= execute - return (collection == other) - else - return false - end - end - - AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }" - # Aggregate 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 counts. - # - # Example: - # - # <tt>criteria.only(:field1).where(:field1 => "Title").aggregate</tt> - def aggregate - klass.collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE, true) - end - - # Get all the matching documents in the database for the +Criteria+. - # - # Example: - # - # <tt>criteria.all</tt> - # - # Returns: <tt>Array</tt> - def all - collect - end - - # Get the count of matching documents in the database for the +Criteria+. - # - # Example: - # - # <tt>criteria.count</tt> - # - # Returns: <tt>Integer</tt> - def count - @count ||= klass.collection.find(selector, options.dup).count - end - - # Iterate over each +Document+ in the results and pass each document to the - # block. - # - # Example: - # - # <tt>criteria.each { |doc| p doc }</tt> - def each(&block) - @collection ||= execute - if block_given? - container = [] - collection.each do |item| - container << item - yield item - end - @collection = container - 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.each do |(key, value)| - send(key, value) - end - self - 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: - # - # excludes: A +Hash+ where the key is the field name and the value is a - # value that must not be equal to the corresponding field value in the database. - # - # Example: - # - # <tt>criteria.excludes(:field => "value1")</tt> - # - # <tt>criteria.excludes(:field1 => "value1", :field2 => "value1")</tt> - # - # Returns: <tt>self</tt> - def excludes(exclusions = {}) - exclusions.each { |key, value| selector[key] = { "$ne" => value } }; self - end - - # Adds a criterion to the +Criteria+ that specifies additional options - # to be passed to the Ruby driver, in the exact format for the driver. - # - # Options: - # - # extras: A +Hash+ that gets set to the driver options. - # - # Example: - # - # <tt>criteria.extras(:limit => 20, :skip => 40)</tt> - # - # Returns: <tt>self</tt> - def extras(extras) - options.merge!(extras) - filter_options - self - end - - # Adds a criterion to the +Criteria+ that specifies an id that must be matched. - # - # Options: - # - # id_or_object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt> - # - # Example: - # - # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt> - # - # Returns: <tt>self</tt> - def id(id_or_object_id) - if id_or_object_id.kind_of?(String) - id_or_object_id = Mongo::ObjectID.from_string(id_or_object_id) - end - 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". - # - # Options: - # - # inclusions: A +Hash+ where the key is the field name and the value is an - # +Array+ of values that any can match. - # - # Example: - # - # <tt>criteria.in(:field => ["value1", "value2"])</tt> - # - # <tt>criteria.in(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt> - # - # 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 the maximum number of - # results to return. This is mostly used in conjunction with <tt>skip()</tt> - # to handle paginated results. - # - # Options: - # - # value: An +Integer+ specifying the max number of results. Defaults to 20. - # - # Example: - # - # <tt>criteria.limit(100)</tt> - # - # Returns: <tt>self</tt> - def limit(value = 20) - options[:limit] = value; self - 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: - # - # exclusions: A +Hash+ where the key is the field name and the value is an - # +Array+ of values that none can match. - # - # Example: - # - # <tt>criteria.not_in(:field => ["value1", "value2"])</tt> - # - # <tt>criteria.not_in(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt> - # - # Returns: <tt>self</tt> - def not_in(exclusions) - exclusions.each { |key, value| selector[key] = { "$nin" => value } }; self - end - - # Returns the offset option. If a per_page option is in the list then it - # will replace it with a skip parameter and return the same value. Defaults - # to 20 if nothing was provided. - def offset - options[:skip] - end - - # Adds a criterion to the +Criteria+ that specifies the sort order of - # the returned documents in the database. Similar to a SQL "ORDER BY". - # - # Options: - # - # params: An +Array+ of [field, direction] sorting pairs. - # - # Example: - # - # <tt>criteria.order_by([[:field1, :asc], [:field2, :desc]])</tt> - # - # Returns: <tt>self</tt> - def order_by(params = []) - options[:sort] = params; self - end - - # Either returns the page option and removes it from the options, or - # returns a default value of 1. - def page - if options[:skip] && options[:limit] - (options[:skip].to_i + options[:limit].to_i) / options[:limit].to_i - else - 1 - end - end - - # Executes the +Criteria+ and paginates the results. - # - # Example: - # - # <tt>criteria.paginate</tt> - def paginate - @collection ||= execute - WillPaginate::Collection.create(page, per_page, count) do |pager| - pager.replace(collection.to_a) - end - end - - # Returns the number of results per page or the default of 20. - def per_page - (options[:limit] || 20).to_i - end - - # Adds a criterion to the +Criteria+ that specifies the fields that will - # get returned from the Document. Used mainly for list views that do not - # require all fields to be present. This is similar to SQL "SELECT" values. - # - # Options: - # - # args: A list of field names to retrict the returned fields to. - # - # Example: - # - # <tt>criteria.only(:field1, :field2, :field3)</tt> - # - # Returns: <tt>self</tt> - 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 - # <tt>limit()</tt> to handle paginated results, and is similar to the - # traditional "offset" parameter. - # - # Options: - # - # value: An +Integer+ specifying the number of results to skip. Defaults to 0. - # - # Example: - # - # <tt>criteria.skip(20)</tt> - # - # 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. - # - # If the passed in args are a type and a hash, then it will construct - # the +Criteria+ with the proper selector, options, and type. - # - # Options: - # - # args: either a +String+ or a +Symbol+, +Hash combination. - # - # Example: - # - # <tt>Criteria.translate(Person, "4ab2bc4b8ad548971900005c")</tt> - # - # <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 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 - # collection itself will be retrieved from the class provided. - # - # Returns either a cursor or an empty array. - def execute - cursor = klass.collection.find(selector, options.dup) - if cursor - @count = cursor.count - cursor - else - [] - end - end - - # Filters the unused options out of the options +Hash+. Currently this - # takes into account the "page" and "per_page" options that would be passed - # in if using will_paginate. - def filter_options - page_num = options.delete(:page) - per_page_num = options.delete(:per_page) - if (page_num || per_page_num) - options[:limit] = (per_page_num || 20).to_i - options[:skip] = (page_num || 1).to_i * options[:limit] - options[:limit] - end - end - - def self.invert(order) - SORT_REVERSALS[order] - end + delegate \ + :and, + :any_in, + :cache, + :enslave, + :excludes, + :extras, + :id, + :in, + :limit, + :not_in, + :offset, + :only, + :order_by, + :page, + :per_page, + :skip, + :where, :to => :criteria end end + +Hash.send(:include, Mongoid::Extensions::Hash::CriteriaHelpers) +Symbol.send(:include, Mongoid::Extensions::Symbol::Inflections)