Ruby nested const_missing method_missing stack too deep

379 views Asked by At

I have the following class:

module APIWrapper
  include HTTParty
  BASE_URI = 'https://example.com/Api'

  def self.const_missing(const_name)
    anon_class = Class.new do
      def self.method_missing method_name, *params
        params = {
          'Target' => const_name.to_s,
          'Method' => method_name.to_s,
        }

        APIWrapper.call_get params
      end
    end
  end

  def self.call_get(params)
    get(APIWrapper::BASE_URI, {:query => params})
  end

  def self.call_post(params)
    post(APIWrapper::BASE_URI, params)
  end
end

I want to be able to make a call to my wrapper like this:

APIWrapper::User::getAll

I'm getting a stack level too deep error:

1) Error:
test_User_getAll(APITest):
SystemStackError: stack level too deep
api_test.rb:16

What am I doing wrong?

3

There are 3 answers

7
lmars On BEST ANSWER

After using the keyword def, a new scope is created, so the issue here is that the const_name variable is no longer in scope inside the body of the method_missing method.

You can keep the variable in scope by using blocks like so:

def self.const_missing(const_name)                                                                                                                             
  anon_class = Class.new do                                                                                                                                    
    define_singleton_method(:method_missing) do |method_name, *params|                                                                                                 
      params = {                                                                                                                                             
        'Target' => const_name.to_s,                                                                                                                         
        'Method' => method_name.to_s,                                                                                                                        
      }                                                                                                                                                      

      APIWrapper.call_get params                                                                                                                                                                                                                                                                                   
    end                                                                                                                                                        
  end                                                                                                                                                          
end                                                                                                                                                            

You might want to also set the constant to the anonymous class you just created:

anon_class = Class.new do
  ...
end

const_set const_name, anon_class
1
Alex.Bullard On

The problem is that const_name is recursively calling method_missing. When you pass a block to Class.new the block is evaluated in the scope of the class. (See the docs)

1
Simon Perepelitsa On

Methods do not see any local outside variables. In your case it is const_name, which is triggering method_missing recursively.

name = "Semyon"
def greet
  puts "hello, #{name}!"
end
greet # undefined local variable or method ‘name’

You can name anonymous modules (and classes) in Ruby using const_set, and from there you can easily see the name. I'd also not recommend defining new methods for every class, this is what modules are for. Here is my short, self-contained example:

module Greeting
  module Base
    def method_missing(word)
      Greeting.greet word, self.name.split("::").last
    end
  end

  def self.greet(word, name)
    puts "#{word}, #{name}!"
  end

  def self.const_missing(name)
    const_set name, Module.new.extend(Base)
  end
end

Greeting::Semyon.hello # hello, Semyon!