# encoding: utf-8 require "mongoid/criterion/complex" require "mongoid/criterion/exclusion" require "mongoid/criterion/inclusion" require "mongoid/criterion/optional" module Mongoid #: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 Mongo::Collection # 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: # # criteria = Criteria.new # # criteria.only(:field).where(:field => "value").skip(20).limit(20) # # criteria.execute class Criteria include Criterion::Exclusion include Criterion::Inclusion include Criterion::Optional include Enumerable attr_reader :collection, :ids, :klass, :options, :selector attr_accessor :documents delegate :aggregate, :avg, :blank?, :count, :distinct, :empty?, :execute, :first, :group, :id_criteria, :last, :max, :min, :one, :page, :paginate, :per_page, :sum, :to => :context # Concatinate the criteria with another enumerable. If the other is a # +Criteria+ then it needs to get the collection from it. def +(other) entries + comparable(other) end # Returns the difference between the criteria and another enumerable. If # the other is a +Criteria+ then it needs to get the collection from it. def -(other) entries - comparable(other) 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 return (execute.entries == other) else return false end end # Return or create the context in which this criteria should be executed. # # This will return an Enumerable context if the class is embedded, # otherwise it will return a Mongo context for root classes. def context @context ||= Contexts.context_for(self) end # Iterate over each +Document+ in the results. This can take an optional # block to pass to each argument in the results. # # Example: # # criteria.each { |doc| p doc } def each(&block) context.iterate(&block) self end # Merges the supplied argument hash into a single criteria # # Options: # # criteria_conditions: Hash of criteria keys, and parameter values # # Example: # # criteria.fuse(:where => { :field => "value"}, :limit => 20) # # Returns self def fuse(criteria_conditions = {}) criteria_conditions.inject(self) do |criteria, (key, value)| criteria.send(key, value) end 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, @documents = {}, {}, klass, [] 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: # # criteria.merge({ :conditions => { :title => "Sir" } }) def merge(other) @selector.update(other.selector) @options.update(other.options) @documents = other.documents 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. # # Returns: Criteria def method_missing(name, *args) if @klass.respond_to?(name) new_scope = @klass.send(name, *args) new_scope.merge(self) return new_scope else return entries.send(name, *args) end end alias :to_ary :to_a # Returns the selector and options as a +Hash+ that would be passed to a # scope for use with named scopes. def scoped { :where => @selector }.merge(@options) end # 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: # # Criteria.translate(Person, "4ab2bc4b8ad548971900005c") # Criteria.translate(Person, :conditions => { :field => "value"}, :limit => 20) def self.translate(*args) klass = args[0] params = args[1] || {} unless params.is_a?(Hash) return klass.criteria.id_criteria(params) end conditions = params.delete(:conditions) || {} if conditions.include?(:id) conditions[:_id] = conditions[:id] conditions.delete(:id) end return klass.criteria.where(conditions).extras(params) end protected # 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. # # Example: # # Given a criteria with a selector of { :page => 1, :per_page => 40 } # # criteria.filter_options # selector: { :skip => 0, :limit => 40 } def filter_options page_num = @options.delete(:page) per_page_num = @options.delete(:per_page) if (page_num || per_page_num) @options[:limit] = limits = (per_page_num || 20).to_i @options[:skip] = (page_num || 1).to_i * limits - limits end end # Return the entries of the other criteria or the object. Used for # comparing criteria or an enumerable. def comparable(other) other.is_a?(Criteria) ? other.entries : other end # Update the selector setting the operator on the value for each key in the # supplied attributes +Hash+. # # Example: # # criteria.update_selector({ :field => "value" }, "$in") def update_selector(attributes, operator) attributes.each do |key, value| unless @selector[key] @selector[key] = { operator => value } else new_value = @selector[key].values.first + value @selector[key] = { operator => new_value } end end; self end end end