Monkey patch a private method of ActiveStorage::VariantWithRecord

I’m struggling to backport this feature that we really need:

Basically I just need to change this method:

ActiveStorage::VariantWithRecord#record

It is a private method.

Is there any way to change it in our application?

I have create an initializer and defined the class ActiveStorage::VariantWithRecord… however this overwrites the whole class. Is there any way to overwrite only the single method?

I have already tried:

  • ActiveStorage::VariantWithRecord.include VariantWithRecordPatch
  • ActiveStorage::VariantWithRecord.prepend VariantWithRecordPatch
  • ActiveStorage::VariantWithRecord.send(:undef_method, :record) ActiveStorage::VariantWithRecord.send(:define_method, :record, ->() { }) ActiveStorage::VariantWithRecord.send(:private, :record)

… nothing seems to work :frowning: No effect.

The only solution would be to overwrite the entire class, but I’m wondering if there’s a way to overwrite only that specific method (which is private).

This is definitely a weird behavior of Ruby on Rails…

If I make some tests with a simple Ruby file everything works as expected.

Example:

class Human

  def public_speak
    speak
  end

  private

  def speak 
    puts "Hello"
  end
end

john = Human.new
john.public_speak


class Human

  private

  def speak 
    puts "Hi"
  end
end

john.public_speak

The above code works as expected (prints “Hello”, then “Hi”).

However if I use the same strategy to overwrite a Rails core method everything breaks:

class ActiveStorage::VariantWithRecord

  private

  def record
    puts "This is the new record!!!"

    @record ||= if blob.variant_records.loaded?
      blob.variant_records.find { |v| v.variation_digest == variation.digest }
    else
      blob.variant_records.find_by(variation_digest: variation.digest)
    end
  end

end

This code also changes initialize and other methods and I get weird errors like wrong number of arguments (given 2, expected 0) that have nothing to do with the overwritten method.

I think it might be related to preloading or something. What is the correct strategy to change a method in Rails?

This is very likely the source of all evil:

However I can’t find a reliable solution.

Don’t know if it corresponds to your issue but this is how I’ve been monkey-patching stuff in Rails:

Thank you!

In the meantime I have found this solution:

# config/application.rb

    config.to_prepare do
      ActiveStorage::VariantWithRecord.class_eval do
        private
        def record
           # the new code
        end
    end

It seems to work for now (not tested in production yet). This code is simpler than yours, but I don’t know if it has any drawbacks.

When a reload happens, the class ActiveStorage::VariantWithRecord is reloaded too, because that is a model of the engine.

Your application autoloaded the class during initialization to change it. But on reload, the class is totaly new again, and since initializers do not run on reload, that new one does not have your decoration anymore.

As documented in the guide you linked to, when you want to do something on boot and on each reload, you need to wrap that in a to_prepare block. Your solution is correct.

In Rails 7, attempting to autoload a reloadable class or module during initialization is going to be an error condition, to force you to write it right, instead of wondering why it does not work. (However, starting with Rails 6, a warning is issued explaining the problem.)