Search code examples
rubyoopmetaprogrammingsequel

Dynamic classes in Ruby, Class.new, unpacking Sequel::Model


I am new to ruby and trying to figure out how the class declaration below from Sequel gem works behind the scene.

class Enumeration < Sequel::Model(DB[:enumerations]); end

Upon quick investigation of Sequel gem's code, it seems to me that the module method Sequel::Model returns a class instance with a configured class attribute. The return instance of Class is then used in inheritance hierarchy, so I tried to test my understanding through the code;

module MySequel
  class MyModel
    module ClassMethods
      attr_accessor :table_name
  
      
      def model(table_name)
        klass = Class.new(self)
                
        klass.table_name = table_name
        
        puts klass.table_name # prints table_name, it does get set for the first class Class object klass
        
        klass
      end
    end
    
    extend ClassMethods
  end
end

class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end

p Enumeration.table_name        # prints nil
p EnumerationValue.table_name   # prints nil

Based on my understanding the class variable table_name gets set while creating an instance of Class and gets propagated into it's child class. However, that doesn't seem to be the case.

Can someone please explain the concept behind Sequel::Model and the problem with my sample implementation to achieve the same result.

The behaviour in this example is similar to when using the anonymous classes above.

class TestModel

  @@table_name = 'test'
  
  def TestModel.table_name
  @@table_name
  end
  
  def TestModel.table_name=(value)
  @@table_name = value
  end
end

TestModel.table_name = 'posts'
p TestModel.table_name    # prints posts


class ChildTestModel < TestModel; end

puts Enumeration.table_name   # prints nil

The Sequel library is seemingly propagating the information about the dataset set through call to module method to the child class (through anonymous class.)

However, I can't figure out; what construct of the Ruby language allows the Status class below to know that it's restricted to a record in table enumerations with name set to 'status'.

class Status < Sequel::Model(DB[:enumerations].where(name: 'status')); end

Solution

  • The Sequel library is seemingly propagating the information about the dataset set through call to module method to the child class

    Yes, indeed. Sequel uses the inherited callback to copy the class instance variables over to the subclass (see lib/sequel/model/base.rb#843).

    Here's a very basic version for your example class:

    module MySequel
      class MyModel
        module ClassMethods
          attr_accessor :table_name
    
          def model(table_name)
            klass = Class.new(self)
            klass.table_name = table_name
            klass
          end
    
          def inherited(subclass)
            instance_variables.each do |var|
              value = instance_variable_get(var)
              subclass.instance_variable_set(var, value)
            end
          end
        end
    
        extend ClassMethods
      end
    end
    

    As a result, the subclasses will have their class instance variables (here @table_name) set to the same values as the (anonymous) parent class:

    class Enumeration < MySequel::MyModel.model(:enumerations); end
    class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end
    
    p Enumeration.table_name      #=> :enumerations
    p EnumerationValue.table_name #=> :enumeration_values