Rails: Is that possible to define named scope in a module? - ruby-on-rails

Say there are 3 models: A, B, and C. Each of these models has the x attribute.
Is that possible to define a named scope in a module and include this module in A, B, and C ?
I tried to do so and got an error message saying that scope is not recognized...

Yes it is
module Foo
def self.included(base)
base.class_eval do
scope :your_scope, lambda {}
end
end
end

As of Rails 3.1 the syntax is simplified a little by ActiveSupport::Concern:
Now you can do
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
scope :disabled, where(:disabled => true)
end
module ClassMethods
...
end
end
ActiveSupport::Concern also sweeps in the dependencies of the included module, here is the documentation
[update, addressing aceofbassgreg's comment]
The Rails 3.1 and later ActiveSupport::Concern allows an include module's instance methods to be included directly, so that it's not necessary to create an InstanceMethods module inside the included module. Also it's no longer necessary in Rails 3.1 and later to include M::InstanceMethods and extend M::ClassMethods. So we can have simpler code like this:
require 'active_support/concern'
module M
extend ActiveSupport::Concern
# foo will be an instance method when M is "include"'d in another class
def foo
"bar"
end
module ClassMethods
# the baz method will be included as a class method on any class that "include"s M
def baz
"qux"
end
end
end
class Test
# this is all that is required! It's a beautiful thing!
include M
end
Test.new.foo # ->"bar"
Test.baz # -> "qux"

As for Rails 4.x you can use gem scopes_rails
It can generate scopes file and include it to your model.
Also, it can automatically generate scopes for state_machines states.

Related

ActiveSupport::Concern should be included or extended

I know include is used for access module methods as instance methods while extend is used to access module methods as class methods.
For ActiveSupport::Concern somewhere I see written as,
module Test
include ActiveSupport::Concern
end
while at some places written as,
module Test
extend ActiveSupport::Concern
end
Here my confusion is, ActiveSupport::Concern should be used with include or with extend?
You should use extend ActiveSupport::Concern, like shown in the examples in the documentation.
By using ActiveSupport::Concern the above module could instead be
written as:
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
end
class_methods do
...
end
end
The reason for using extend is to make the methods defined in ActiveSupport::Concern available in module context. This allows you to use the methods included and class_methods within the module.
When using include those methods would not be available within the module and instead be available on instances of a class that includes M.
If you want to know the difference between the two I suggest taking a look at What is the difference between include and extend in Ruby?
You need to extend the module with ActiveSupport::Concern in order for the ActiveSupport::Concern#included and #class_methods methods to work properly.
These two methods are after all pretty much the only reason for its existance.
module A
extend ActiveSupport::Concern
# raises ArgumentError (wrong number of arguments (given 0, expected 1))
included do
puts "Hello World"
end
end
module B
extend ActiveSupport::Concern
included do
puts "Hello World"
end
end
class C
include B
end
# Outputs Hello World
Check out what happens if we inspect the included method:
module AB
include ActiveSupport::Concern
puts method(:included).source_location # nil
end
module ABC
extend ActiveSupport::Concern
puts method(:included).source_location # .../ruby/gems/2.7.0/gems/activesupport-6.0.2.1/lib/active_support/concern.rb
end
When we extend the module with ActiveSupport::Concern we are putting it on the ancestors chain of ABC, thus the methods of ActiveSupport::Concern are available as module methods of ABC. This does not happen when you use include and the included method called is actually Module#included from the Ruby core.

ActiveSupport::Concern methods not found

I am extending ActiveRecord::Base. In my lib folder, I have a file mongoid_bridge.rb:
module MongoidBridge
extend ActiveSupport::Concern
module ClassMethods
...
end
module InstanceMethods
...
def create_mongo(klass, fields)
...
end
end
end
ActiveRecord::Base.send(:include, MongoidBridge)
In config/initializers, I have two files, in order to be read in the correct order, each is prefixed with 01, 02, etc. In 01_mongo_mixer.rb, I have the following:
require "active_record_bridge"
require "mongoid_bridge"
Then in 02_model_initializer.rb, I have the following:
MyActiveRecordModel.all.each do |model|
model.create_mongo(some_klass, some_fields)
end
model is an instance of an ActiveRecord subclass, so it should find create_mongo instance method in the lookup chain. However, it does not find it, as I get the following error:
Uncaught exception: undefined method `create_mongo' for #<MyActiveRecordModel:0x007fff1f5e5e18>
Why can't it find the instance method?
UPDATE:
It seems that methods under ClassMethods are included, but not the methods under InstanceMethods:
singleton_respond = MyActiveRecordModel.respond_to? :has_many_documents
# => true
instance_respond = MyActiveRecordModel.new.respond_to? :create_mongo
# => false
You don't need an InstanceMethods module - your module should look like
module MongoidBridge
extend ActiveSupport::Concern
module ClassMethods
...
end
def create_mongo
end
end
Earlier versions of rails made use of an instance methods module but in the end it was decided that this was redundant since you could just define the methods in the enclosing module. Using InstanceMethods was deprecated a while back (maybe rails 3.2 - my memory is fuzzy) and subsequently removed

Rails: Including a Concern with a constant within a Concern

I have concern in which I store constants:
module Group::Constants
extend ActiveSupport::Concern
MEMBERSHIP_STATUSES = %w(accepted invited requested
rejected_by_group rejected_group)
end
And another concern that I wish to use these constants:
module User::Groupable
extend ActiveSupport::Concern
include Group::Constants
MEMBERSHIP_STATUSES.each do |status_name|
define_method "#{status_name}_groups" do
groups.where(:user_memberships => {:status => status_name})
end
end
end
Unfortunately, this results in a routing error:
uninitialized constant User::Groupable::MEMBERSHIP_STATUSES
It looks like the first concern isn't loading properly in the second concern. If that's the case, what can I do about it?
It appears this behavior is by design, as explained nicely over here.
What you'll need to do in this case is not have Group::Constants extend from ActiveSupport::Concern since that will block its implementation from being shared with other ActiveSupport::Concern extending modules (although it will be ultimately shared in a class that includes the second module):
module A
TEST_A = 'foo'
end
module B
extend ActiveSupport::Concern
TEST_B = 'bar'
end
module C
extend ActiveSupport::Concern
include A
include B
end
C::TEST_A
=> 'foo'
C::TEST_B
=> uninitialized constant C::TEST_B
class D
include C
end
D::TEST_A
=> 'foo'
D::TEST_B
=> 'bar'
In short, you'll need to make Group::Constants a standard module and then all will be well.
If you want to keep everything in one file, and if you can stomach a bit of boilerplate, you could break up your module into a "concern" bit and a "non-concern" bit:
module A
FOO = [22]
def self.included base
base.include Concern
end
module Concern
extend ActiveSupport::Concern
class_methods do
def foo_from_the_perspective_of_a_class_method_in_A
{lexical: FOO, instance: self::FOO}
end
end
end
end
module B
extend ActiveSupport::Concern
include A
FOO += [33]
def foo_from_the_perspective_of_an_instance_method_in_B
FOO
end
end
class C
include B
end
C.foo_from_the_perspective_of_a_class_method_in_A
=> {:lexical=>[22], :instance=>[22, 33]}
C.new.foo_from_the_perspective_of_an_instance_method_in_B
=> [22, 33]
C::FOO
=> [22, 33]

How to "nest" the inclusion of modules when using the Ruby on Rails ActiveSupport::Concern feature?

I am using Ruby 1.9.2 and the Ruby on Rails v3.2.2 gem. I would like to "nest" the inclusion of modules given I am using the RoR ActiveSupport::Concern feature, but I have a doubt where I should state the include method. That is, I have the following:
module MyModuleA
extend ActiveSupport::Concern
# include MyModuleB
included do
# include MyModuleB
end
end
Should I state include MyModuleB in the "body" / "context" / "scope" of MyModuleA or I should state that in the included do ... end block? What is the difference and what I should expect from that?
If you include MyModuleB in the "body" of MyModuleA, then it is the module itself that is extended with B's functionality. If you include it in the included block, then it is included on the class that mixes in MyModuleA.
That is:
module MyModuleA
extend ActiveSupport::Concern
include MyModuleB
end
produces something like:
MyModuleA.send :include, MyModuleB
class Foo
include MyModuleA
end
while
module MyModuleA
extend ActiveSupport::Concern
included do
include MyModuleB
end
end
produces something like:
class Foo
include MyModuleA
include MyModuleB
end
The reason for this is that ActiveSupport::Concern::included is analogous to:
def MyModuleA
def self.included(klass, &block)
klass.instance_eval(&block)
end
end
The code in the included block is run in the context of the including class, rather than the context of the module. Thus, if MyModuleB needs access to the class it's being mixed-in to, then you'd want to run it in the included block. Otherwise, it's effectively the same thing.
By means of demonstration:
module A
def self.included(other)
other.send :include, B
end
end
module B
def self.included(other)
puts "B was included on #{other.inspect}"
end
end
module C
include B
end
class Foo
include A
end
# Output:
# B was included on C
# B was included on Foo

Why is ruby responding that I've already mixed in a module when I haven't included it yet?

I have a class Ryte::Theme. If I call included_modules on the class I get back (abbreviated):
[Ryte::Bundleable::Core, Ryte::Bundleable::Validators,
Ryte::Bundleable::Builder, ActiveModel::Validations::HelperMethods,
ActiveModel::Validations, ActiveSupport::Callbacks, Ryte::Bundleable]
Ryte::Theme is pulling in nested modules through a single module Ryte::Bundleable. Here are the relevant class and module definitions:
class Ryte::Theme
include Ryte::Bundleable
end
module Ryte::Bundleable
extend ActiveSupport::Concern
included do
include ActiveModel::Validations
include Ryte::Bundleable::Builder
include Ryte::Bundleable::Validators
include Ryte::Bundleable::Core
end
end
Given this, why is it I receive the following response:
Ryte::Theme.include?(Ryte::Theme::Validators)
=> true
I have not included this additional module (yet).. This is evident in the included_modules response. Is this related to ActiveSupport Concern? I want to be able to include Ryte::Theme::Validators and have it mixin as well but since it thinks it is already included it does not include it 'again' (as it shouldn't if that were true). As such it gets left behind when I add the include to the class definition, like so:
class Ryte::Theme
include Ryte::Bundleable
include Ryte::Theme::Validators # <- Does not load
end
Why is this additional module Ryte::Theme::Validators not mixing in as well?
ADDED
Ok just realized:
1.9.3p194 :005 > Ryte::Bundleable::Validators == Ryte::Theme::Validators
=> true
Odd.. why is this?
UPDATE: This is related to ActiveSupport::Concern (see below).
Try the following:
# initialize constants
class A; end # class Ryte
module A::B; end # module Ryte::Bundleable
module A::B::C; end # module Ryte::Bundleable::Validators
module A::B # module Ryte::Bundleable
def self.include(base)
base.class_eval do
include A::B::C # include Ryte::Bundleable::Validators
end
end
end
class D # class Ryte::Theme
include A::B # include Ryte::Bundleable
end
A::B::C == D::C #=> true
This happens because of the way the modules and classes are namespaced: when you include A::B::C from inside of A::B, the module name is referenced relative to the module itself, which becomes just C (in your case, just Validators). Thus when you include A::B in some other class D, rather than including a module named A::B::C (i.e. Ryte::Bundleable::Validators), ruby includes a module named just C (i.e. Validators). This is why Ryte::Bundleable::Validators == Ryte::Theme::Validators evaluates to true.
However, with the example above:
D.include?(A::B::C) #=> false
So this is where ActiveSupport::Concern kicks in. Redefine the module A::B above as follows:
module A::B # module Ryte::Bundleable
extend ActiveSupport::Concern
included do
include A::B::C # include Ryte::Bundleable::Validators
end
end
And you will find that D.include?(A::B::C) now evaluates to true. I'm honestly not sure why this happens, but it must have something to do with the namespacing above.

Resources