1

I'm using the rails-settings gem, and I'm trying to understand how you add functions to ActiveRecord classes (I'm building my own library for card games), and I noticed that this gem uses one of the Meta-programming techniques to add the function to the ActiveRecord::Base class (I'm far from Meta-programming master in ruby, but I'm trying to learn it)

module RailsSettings
  class Railtie < Rails::Railtie

    initializer 'rails_settings.initialize', :after => :after_initialize do
      Railtie.extend_active_record
    end

  end

  class Railtie
    def self.extend_active_record
      ActiveRecord::Base.class_eval do
        def self.has_settings
          class_eval do
            def settings
              RailsSettings::ScopedSettings.for_thing(self)
            end

            scope :with_settings, :joins => "JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                               settings.thing_type = '#{self.base_class.name}')",
                                  :select => "DISTINCT #{self.table_name}.*"

            scope :with_settings_for, lambda { |var| { :joins => "JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                                                    settings.thing_type = '#{self.base_class.name}') AND
                                                                                    settings.var = '#{var}'" } }

            scope :without_settings, :joins => "LEFT JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                                       settings.thing_type = '#{self.base_class.name}')",
                                     :conditions => 'settings.id IS NULL'

            scope :without_settings_for, lambda { |var| { :joins => "LEFT JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                                                            settings.thing_type = '#{self.base_class.name}') AND
                                                                                            settings.var = '#{var}'",
                                                          :conditions => 'settings.id IS NULL' } }
          end
        end
      end
    end

  end
end

What I don't understand is why he uses class_eval on ActiveRecord::Base, wasn't it easier if he just open the ActiveRecord::Base class and define the functions? Specially that there's nothing dynamic in the block (What I mean by dynamic is when you do class_eval or instance_eval on a string containing variables)

something like this:

module ActiveRecord
  class Base
    def self.has_settings
      class_eval do
        def settings
          RailsSettings::ScopedSettings.for_thing(self)
        end

        scope :with_settings, :joins => "JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                           settings.thing_type = '#{self.base_class.name}')",
                              :select => "DISTINCT #{self.table_name}.*"

        scope :with_settings_for, lambda { |var| { :joins => "JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                                                settings.thing_type = '#{self.base_class.name}') AND
                                                                                settings.var = '#{var}'" } }

        scope :without_settings, :joins => "LEFT JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                                   settings.thing_type = '#{self.base_class.name}')",
                                 :conditions => 'settings.id IS NULL'

        scope :without_settings_for, lambda { |var| { :joins => "LEFT JOIN settings ON (settings.thing_id = #{self.table_name}.#{self.primary_key} AND
                                                                                        settings.thing_type = '#{self.base_class.name}') AND
                                                                                        settings.var = '#{var}'",
                                                      :conditions => 'settings.id IS NULL' } }
      end
    end
  end
end

I understand the second class_eval (before the def settings) is to define functions on the fly on every class that 'has_settings' right ? Same question here, I think he could use "def self.settings" instead of "class_eval.... def settings", no ?

kalbasit
  • 766
  • 8
  • 17

1 Answers1

0

What rails-settings code does is considered good practice: it only messes with third-party modules when it's explicitely asked to do so. This way you also keep the namespaces tidily separated and all your code remains in your modules.

tokland
  • 66,169
  • 13
  • 144
  • 170
  • You didn't asnwer my question, I know it's a good practice, and that can bee seen by the second class_eval, the one just before "def settings", but my question was about the first class_eval, using it or opening the class has the same effect no ? – kalbasit Jan 16 '11 at 11:49
  • 1
    @eMxyzptlk: do you mean doing a "class xyz" inside a method? Ruby does not allow it (error: "class definition in method body") – tokland Jan 16 '11 at 12:08
  • @eMxyzptlk: I think it just adds another level of on-demand method definition. By putting this on Railtie instead of ActiveRecord itself, it's possible to require this library without even adding `has_settings` to ActiveRecord until it's explicitly requested, which in turn gives the option of defining `settings` if explicitly requested. – Jimmy Jan 16 '11 at 12:54
  • I understand that if you open a class definition inside a function definition you get an error, it makes sence, but if you look at the code, the whole code waits for Rails to call Railtie.extend_active_record which is turn opens the ActiveRecord::Base class and extend id, however IMO it would be simple to replace the whole code (first version in my question) with the second one, don't you agree? Both versions extend the ActiveRecord::Base class, both add the settings and the scope to the class that calls has_settings explicitly, but the second one is simpler.. – kalbasit Jan 16 '11 at 18:55
  • 1
    i didn't read everything. however, maybe this helps you. `Foo.class_eval { ... }` and `class Foo; ... end` are basically equivalent. – Christoph Schiessl Jan 16 '11 at 21:25
  • Thank you Christoph, that was exactly what I wanted to know, so basically, he used class_eval as a matter of taste or perhaps convention, but it's basically the same.. Thank you! – kalbasit Jan 17 '11 at 15:27