Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining class variables inside class_eval inside Module#included

Tags:

ruby

mongodb

How does one define class variables inside a class_eval block? I have the following:

module Persist
    def self.included(base)
        # base is the class including this module
        base.class_eval do
            # class context begin
            @@collection = Connection.new.db('nameofdb').collection(self.to_s.downcase)
            def self.get id # Class method
                #...
            end
        end
    end
    # Instance methods follow
    def find
        @@collection.find().first
        #...
    end
end

class User
    include Persist
end

class Post
    include Persist
end

The classes User and Post both show :get when introspecting using User.methods or Post.methods. Which makes sense, as they are defined in the context of class_eval and is exactly what I need. Similarly the method :find is shown as an instance_method of the individual classes.

However, what I thought was a class variable i.e. @@collection, turns out to be a Module level class_variable. When I introspect User.class_variables or Post.class_variables, they turn up empty. However Persist.class_variables shows :@@collection.

How is this possible? Isn't the context inside the class_eval block that of the class. So shouldn't the variable @@collection get defined on the class and not the module?

Also, the value of @@collection is always the last Class's name that included it. So in this case it is always 'posts' and never 'users'. I think this is because it's a module level variable, it will change on every include. Is this correct?

Finally how would I define a class variable in this context so that each class has it's own @@collection definition.

like image 249
Capstone Avatar asked Feb 17 '13 13:02

Capstone


1 Answers

One method would be to create accessor methods for the class variable.

module Persist
    def self.included(base)
        # Adds class methods from 'ClassMethods' to including class.
        base.extend(ClassMethods)
        base.class_eval do
            self.collection = Connection.new.db('nameofdb').collection(self.to_s.downcase)
            # ...
        end
    end
    module ClassMethods
        def collection=(value)
            @@collection = value
        end
        def collection
            @@collection
        end
    end
    # Instance methods follow
    def find
        self.class.collection.find().first
        #...
    end
end

class User
    include Persist
end

class Post
    include Persist
end

Another method would be to access the class variables of the including class in the module via the accessors class_variable_set, etc.

def self.included(base)
    base.class_eval do
        class_variable_set('@@collection', Connection.new.db('nameofdb').collection(self.to_s.downcase))
        # …
    end
end

I'll take a stab at answering your question "How is this possible? Isn't the context inside the class_eval block that of the class."

The class_eval method does make self refer to the class it's being invoked on inside the given block. This allows you to invoke class methods, etc. However, class variables will be evaluated in the context that the block is bound to - here, the module.

For instance, try doing this:

class Foo
    @@bar = 1
end

Foo.class_eval { puts @@bar }

This would result in the exception "NameError: uninitialized class variable @@bar in Object". Here the given block is bound to the context of "Object", the top-level namespace.

like image 173
Rich Drummond Avatar answered Oct 17 '22 07:10

Rich Drummond