0

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?

kid_drew
  • 3,857
  • 6
  • 28
  • 38

3 Answers3

3

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
lmars
  • 2,502
  • 1
  • 16
  • 9
  • That's a terrible construct: you open the singleton class to get self, which you then use to change the current class to the singleton class. – 7stud Sep 09 '13 at 23:00
  • how else would you keep the const_name variable in scope and add a method to the class's singleton class? – lmars Sep 09 '13 at 23:03
  • self.instance_eval do {...}, although now that I think about it more carefully, when you use class_eval you may have to do what you did. – 7stud Sep 09 '13 at 23:05
  • The class keyword creates a new scope, so the const_name variable will be out of scope – lmars Sep 09 '13 at 23:06
  • Yes, you are right. Please accept my apologies. Just use instance_eval and avoid all that. instance_eval changes the current class to the receiver's singleton class. – 7stud Sep 09 '13 at 23:09
  • 1
    Actually, you don't even need instance_eval because there is a public method named define_singelton_method(), which will create a method in the singleton class of the receiver. – 7stud Sep 09 '13 at 23:16
  • @7stud yes you are correct I forgot about `define_singleton_method`, I will update the answer, thanks! – lmars Sep 09 '13 at 23:21
0

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)

Alex.Bullard
  • 5,533
  • 2
  • 25
  • 32
0

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!
Simon Perepelitsa
  • 20,350
  • 8
  • 55
  • 74