require 'active_support/core_ext/array'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/kernel/singleton_class'
module ActiveRecord
# = Active Record \Named \Scopes
module Scoping
module Named
extend ActiveSupport::Concern
module ClassMethods
# Returns an ActiveRecord::Relation scope object.
#
# posts = Post.all
# posts.size # Fires "select count(*) from posts" and returns the count
# posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
#
# fruits = Fruit.all
# fruits = fruits.where(color: 'red') if options[:red_only]
# fruits = fruits.limit(10) if limited?
#
# You can define a scope that applies to all finders using
# ActiveRecord::Base.default_scope.
def all
if current_scope
current_scope.clone
else
default_scoped
end
end
def default_scoped # :nodoc:
relation.merge(build_default_scope)
end
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes # :nodoc:
all.scope_for_create
end
# Are there default attributes associated with this scope?
def scope_attributes? # :nodoc:
current_scope || default_scopes.any?
end
# Adds a class method for retrieving and querying objects. A \scope
# represents a narrowing of a database query, such as
# where(color: :red).select('shirts.*').includes(:washing_instructions).
#
# class Shirt < ActiveRecord::Base
# scope :red, -> { where(color: 'red') }
# scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
# end
#
# The above calls to +scope+ define class methods Shirt.red and
# Shirt.dry_clean_only. Shirt.red, in effect,
# represents the query Shirt.where(color: 'red').
#
# You should always pass a callable object to the scopes defined
# with +scope+. This ensures that the scope is re-evaluated each
# time it is called.
#
# Note that this is simply 'syntactic sugar' for defining an actual
# class method:
#
# class Shirt < ActiveRecord::Base
# def self.red
# where(color: 'red')
# end
# end
#
# Unlike Shirt.find(...), however, the object returned by
# Shirt.red is not an Array; it resembles the association object
# constructed by a +has_many+ declaration. For instance, you can invoke
# Shirt.red.first, Shirt.red.count,
# Shirt.red.where(size: 'small'). Also, just as with the
# association objects, named \scopes act like an Array, implementing
# Enumerable; Shirt.red.each(&block), Shirt.red.first,
# and Shirt.red.inject(memo, &block) all behave as if
# Shirt.red really was an Array.
#
# These named \scopes are composable. For instance,
# Shirt.red.dry_clean_only will produce all shirts that are
# both red and dry clean only. Nested finds and calculations also work
# with these compositions: Shirt.red.dry_clean_only.count
# returns the number of garments for which these criteria obtain.
# Similarly with Shirt.red.dry_clean_only.average(:thread_count).
#
# All scopes are available as class methods on the ActiveRecord::Base
# descendant upon which the \scopes were defined. But they are also
# available to +has_many+ associations. If,
#
# class Person < ActiveRecord::Base
# has_many :shirts
# end
#
# then elton.shirts.red.dry_clean_only will return all of
# Elton's red, dry clean only shirts.
#
# \Named scopes can also have extensions, just as with +has_many+
# declarations:
#
# class Shirt < ActiveRecord::Base
# scope :red, -> { where(color: 'red') } do
# def dom_id
# 'red_shirts'
# end
# end
# end
#
# Scopes can also be used while creating/building a record.
#
# class Article < ActiveRecord::Base
# scope :published, -> { where(published: true) }
# end
#
# Article.published.new.published # => true
# Article.published.create.published # => true
#
# \Class methods on your model are automatically available
# on scopes. Assuming the following setup:
#
# class Article < ActiveRecord::Base
# scope :published, -> { where(published: true) }
# scope :featured, -> { where(featured: true) }
#
# def self.latest_article
# order('published_at desc').first
# end
#
# def self.titles
# pluck(:title)
# end
# end
#
# We are able to call the methods like this:
#
# Article.published.featured.latest_article
# Article.featured.titles
def scope(name, body, &block)
unless body.respond_to?(:call)
raise ArgumentError, 'The scope body needs to be callable.'
end
if dangerous_class_method?(name)
raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
"on the model \"#{self.name}\", but Active Record already defined " \
"a class method with the same name."
end
extension = Module.new(&block) if block
singleton_class.send(:define_method, name) do |*args|
scope = all.scoping { body.call(*args) }
scope = scope.extending(extension) if extension
scope || all
end
end
end
end
end
end