Chris C
Chris C

Reputation: 373

Toplevel error accessing Ruby class variable in class_eval

I'm working on a old code Rails 3, Ruby 1.9.3 code base. I references a gem I can't upgrade for various reasons. The gem has already been monkey patched (correct term?) using class_eval. I need to make a change to another method that looks something like:

SomeNamespace::Bar do
  def some_method
    @@part_of_header ||= JSON.dump({... Stuff ...})

    # ... other code ...

    headers = {
      "Header Part" => @@part_of_header
      #... other headers ...
    }
  end
end

The idea behind the @@part_of_header class variable is to cache the JSON dump so it can be reused. The @@part_of_header is NOT defined elsewhere in the Bar base class.

My monkey patched method looks like:

SomeNamespace::Bar.class_eval do
  def some_method
    @@part_of_header ||= JSON.dump({... Stuff ...})

    # ... other code that I changed ...

    headers = {
      "Header Part" => @@part_of_header
      #... other headers that I changed ...
    }
  end
end

The code works fine but I get the following warning on the lines with the @@part_of_header class variable:

Class variable access from toplevel

I tried moving the class variable to it's own method:

SomeNamespace::Bar.class_eval do
  def header_part
    @@part_of_header ||= JSON.dump({... Stuff ...})
  end

  def some_method
    # ... other code that I changed ...

    headers = {
      "Header Part" => header_part
      #... other headers that I changed ...
    }
  end
end

However the "toplevel" error just moved to the header_part method.

I also tried accessing the class variable using class_variable_set and class_variable_get but got undefined method errors.

Any advice on how to fix this warning? If it can't be fixed any advice on caching the JSON dump in the class_eval? Thanks.

Update: Thanks to @Josh for using the full class name with the class_variable_get/set. My final solution looks like:

SomeNamespace::Bar.class_eval do
  def header_part
    # Create the class variable if it does not exist, remember
    # the base class does not define @@part_of_header.
    if !SomeNamesapce::Bar.class_variable_defined?(:@@part_of_header)
      SomeNamespace::Bar.class_variable_set(:@@part_of_header, nil)
    end

     if (SomeNamespace::Bar.class_variable_get(:@@part_of_header).nil?
       header_part = JSON.dump({... Stuff ...})
       SomeNamespace::Bar.class_variable_set(:@@part_of_header, header_part)
     end

     SomeNamespace::Bar.class_variable_get(:@@part_of_header)
  end

  def some_method
    # ... other code that I changed ...

    headers = {
      "Header Part" => header_part
      #... other headers that I changed ...
    }
  end
end

The above works but any feedback on the above solution would be appreciated. Thanks.

Upvotes: 1

Views: 463

Answers (1)

Josh Kelley
Josh Kelley

Reputation: 58342

It looks like the problem is that, although the class_eval do block is executed within the context of SomeNamespace::Bar, that context doesn't apply to references to class variables.

If you explicitly access the class variable, then things should work as expected:

# NOTE: Omitting conditional set (||=) for simplicity
SomeNamespace::Bar::class_variable_set(:@@part_of_header, JSON.dump({... Stuff ...}))

headers = {
  "Header Part" => SomeNamespace::Bar::class_variable_get(:@@part_of_header)
  #... other headers that I changed ...
}

If @@part_of_header is really only used within some_method, and if you're totally replacing some_method, then there's also nothing wrong with you using your own module variable / class variable, instead of reusing the existing SomeNamespace::Bar::@@part_of_header. I might prefer that approach; it feels like it better encapsulates your changes.

module MonkeyPatch
  SomeNamespace::Bar.class_eval do
    def some_method
      # This is within the MonkeyPatch module, so it
      # makes a new class variable for MonkeyPatch
      @@part_of_header ||= "JSON.dump({'a': 12})"

      # ... other code that I changed ...

      headers = {
        "Header Part" => @@part_of_header
        #... other headers that I changed ...
      }
      puts headers
    end
  end
end

Upvotes: 1

Related Questions