Craig Walker
Craig Walker

Reputation: 51717

Defining a method that uses an out-of-scope variable in Ruby

I want to make a Test::Unit test_helper method that I can call to wipe a bunch of tables after the tests execute. Here's the general idea I have:

def self.wipe_models(*models)
  def teardown
    models.each do |model|
      model = model.to_s.camelize.constantize
      model.connection.execute "delete from #{model.table_name}"
    end
  end
end

However, when teardown runs, I get:

undefined local variable or method `models'

To me it looks like the "def" block doesn't obey usual rules for closures; I can't access variables defined outside of its scope.

So, how do I access a variable that's defined outside of a "def" method declaration?

Upvotes: 3

Views: 397

Answers (3)

Guilherme Bernal
Guilherme Bernal

Reputation: 8293

You can do it as a closure with define_method:

def self.wipe_models(*models)
  define_method(:teardown) do
    models.each do |model|
      model = model.to_s.camelize.constantize
      model.connection.execute "delete from #{model.table_name}"
    end
  end
end

Now the method body is a block and can access models.

Upvotes: 4

coreyward
coreyward

Reputation: 80041

Method definitions are not closures in Ruby. The class, module, def, and end keywords are all scope gates. In order to maintain scope across a scope gate you have to pass a block; blocks are closures and thus run in the scope in which they were defined.

def foo
  # since we're in a different scope than the one the block is defined in,
  # setting x here will not affect the result of the yield
  x = 900
  puts yield  #=> outputs "16"
end

# x and the block passed to Proc.new have the same scope
x = 4
square_x = Proc.new { x * x }


foo(&square_x)

Upvotes: 4

Austin Taylor
Austin Taylor

Reputation: 5477

Use a class instance variable:

cattr_accessor :models_to_wipe

def self.wipe_models(*models)
  self.models_to_wipe = models
end

def teardown
  self.class.models_to_wipe.each do |model|
    model = model.to_s.camelize.constantize
    model.connection.execute "delete from #{model.table_name}"
  end
end

Upvotes: 1

Related Questions