malcoauri
malcoauri

Reputation: 12209

method_missing and define_method in Ruby

There is the following code:

class MyOpenStruct
    def initialize(initial_values = {})
    @values = initial_values
    end

    def _singleton_class
        class << self 
            self
        end 
    end

    def method_missing(name, *args, &block) 
        if name[-1] == "="
            base_name = name[0..-2].intern 
            puts "add_method_to_set"
            self.class.add_method_to_set(base_name)
      @values[base_name] = args[0]
     else
            puts "add_method_to_get"
            self.class.add_method_to_get(base_name)         
        @values[name]
        end 
    end

    def self.add_method_to_get(name)
        define_method(name) do |value|
            @values[name]
        end
    end

    def self.add_method_to_set(name)
        define_method(name) do |value|
            @values[name] = value
        end
    end
end

obj1 = MyOpenStruct.new(name: "Dave") 
obj1.address = "1"

obj2 = MyOpenStruct.new(name: "Dave") 
obj2.address = "2"

I want to do the following thing: when I execute some method (obj1.address) and it's missing I want to add this method to my MyOpenStruct class. But when I execute my code I get 'missing' two times instead of one. Why? I don't understand. Please explain it to me. Thanks.

Upvotes: 1

Views: 1172

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110755

@koffeinfrei identified one problem with you code, but I found a few others. Below I have what I believe to be a corrected version. I have also suggested an alternative way to structure the code. My main advice is to pull out the dynamic creation of instance methods, as that is quite generic. You might even put that in a module with other methods that you could include as needed.

Your code with repairs

class MyOpenStruct
  def initialize(initial_values = {})
  @values = initial_values
  end

  def method_missing(name, *args, &block) 
  puts "in mm, name = #{name}"
    if name[-1] == "="
      base_name = name[/\w+/]
      puts "add_method_to_set: '#{name}'"
      self.class.add_method_to_set(base_name)
      @values[base_name.to_sym] = args[0]
    else
      puts "add_method_to_get: '#{name}'"
      self.class.add_method_to_get(name)         
      @values[name.to_sym]
    end 
  end

  def self.add_method_to_get(name)
    define_method(name.to_sym) do
      @values[name.to_sym]
    end
  end

  def self.add_method_to_set(name)
    define_method((name+'=').to_sym) do |value|
      @values[name.to_sym] = value
    end
  end
end

Alternative construction

def create_instance_eval(klass, method, &block)
  klass.class_eval { define_method(method, &block) }
end

class MyOpenStruct
  def initialize(initial_values = {})
    @values = initial_values
  end

  def method_missing(name, *args, &block) 
    if name[-1] == "="
      base_name = name[/\w+/]
      method_name = (base_name+'=').to_sym
      puts "create method '#{method_name}'"
      method = create_instance_eval(self.class, method_name) do |value|
          @values[base_name.to_sym] = value
        end
      send(method, args[0])
    else
      method_name = name.to_sym
      puts "create method '#{method_name}'"
      method = create_instance_eval(self.class, method_name) do
        @values[method_name]
      end
      send(method)
    end 
  end
end

Example

MyOpenStruct.instance_methods(false)
  #=> [:method_missing]

obj1 = MyOpenStruct.new(name: "Dave") 
  #=> #<MyOpenStruct:0x00000102805b58 @values={:name=>"Dave"}>
obj1.address = "1"
  # create method 'address='
  #=> "1"

MyOpenStruct.instance_methods(false)
  #=> [:method_missing, :address=]
obj2 = MyOpenStruct.new(name: "Mitzy") 
  #=> #<MyOpenStruct:0x00000101848878 @values={:name=>"Mitzy"}>
obj2.address = 2
  #=> 2

obj2.address
  # create method 'address'
  # => 2

MyOpenStruct.instance_methods(false)
  $#=> [:method_missing, :address=, :address]

obj1.instance_variable_get(:@values)
  #=> {:name=>"Dave", :address=>"1"}
obj2.instance_variable_get(:@values)
  #=> {:name=>"Mitzy", :address=>2}

Upvotes: 1

koffeinfrei
koffeinfrei

Reputation: 2065

The method name for the setter method needs to have the trailing =, so you need to define the method with the name instead of the base_name.

self.class.add_method_to_set(name)

Upvotes: 0

Related Questions