1

I have a app/extensions folder where my custom exceptions reside and where I extend some of the Ruby/Rails classes. Currently there are two files: exceptions.rb and float.rb.

The folder is specified in the ActiveSupport::Dependencies.autoload_paths:

/Users/mityakoval/rails/efo/app/extensions/**
/Users/mityakoval/rails/efo/app/assets
/Users/mityakoval/rails/efo/app/channels
/Users/mityakoval/rails/efo/app/controllers
/Users/mityakoval/rails/efo/app/controllers/concerns
/Users/mityakoval/rails/efo/app/extensions
/Users/mityakoval/rails/efo/app/helpers
/Users/mityakoval/rails/efo/app/jobs
/Users/mityakoval/rails/efo/app/mailers
/Users/mityakoval/rails/efo/app/models
/Users/mityakoval/rails/efo/app/models/concerns
/Users/mityakoval/rails/efo/app/template.xlsx
/Users/mityakoval/.rvm/gems/ruby-2.4.1@web_app/gems/font-awesome-rails-4.7.0.2/app/assets
/Users/mityakoval/.rvm/gems/ruby-2.4.1@web_app/gems/font-awesome-rails-4.7.0.2/app/helpers
/Users/mityakoval/rails/efo/test/mailers/previews

The reason for it to be listed there twice is that it should be automatically loaded since it was placed under app directory and I have also manually added it to the autoload_paths in application.rb:

config.autoload_paths << File.join(Rails.root, 'app', 'extensions/**')

The strange thing is that my exceptions.rb is successfully loaded at all times, but the float.rb isn't unless eager loading is enabled.

Answers to this question say that it might be related to Spring (which I tend to believe), so I've added the folder to spring.rb:

%w(
  .ruby-version
  .rbenv-vars
  tmp/restart.txt
  tmp/caching-dev.txt
  config/application.yml
  app/extensions
).each { |path| Spring.watch(path) }

I've restarted Spring and the Rails server multiple times and nothing worked. Does anyone have any suggestions?

Ruby version: 2.4.1 Rails version: 5.1.5

EDIT

/Users/mityakoval/rails/efo/app/extensions/float.rb:

class Float
  def comma_sep
    self.to_s.gsub('.', ',')
  end
end

rails console:

irb> num = 29.1
irb> num.comma_sep
NoMethodError: undefined method `comma_sep' for 29.1:Float
    from (irb):2
mityakoval
  • 888
  • 3
  • 12
  • 26
  • The `/app` directory (and its subdirectories) are already on the autoloader path. Sometimes spring has a tendency to get "stuck" and won't pick up newly created directories but I doubt this is what is happening here. You should add the actual constant missing error message and exactly how the classes are named and the file names and paths for us to be able to help you. See https://stackoverflow.com/help/mcve – max May 24 '18 at 12:21
  • You should also note that just because the files are on the autoload path does not mean they are required eagerly. Rails just overloads Module#constant_missing to look for files named according to the constant in the autoload paths. If you are extending core classes you should do it in an initializer. – max May 24 '18 at 12:23
  • Thank you @max. I've added the core extensions in an initializer as you've suggested and it worked. However, when switched back to the files under `app/extensions` and disabled eager_loading the extensions I put in `app/extensions` were still working. So I suppose it was the 'Spring being stuck' issue after all. I'd like to point out that the folder has been there for some time already and I didn't create it just now, only a new file. Is there a way to speed up this loading process? – mityakoval May 24 '18 at 12:53
  • @max, and I've also added the classes and the error message – mityakoval May 24 '18 at 12:55

1 Answers1

2

A better way to monkeypatch a core class is by creating a module and including it in the class to be patched in an initializer:

# /lib/core_extensions/comma_seperated.rb
module CoreExtensions
  module CommaSeperated
    def comma_sep
      self.to_s.gsub('.', ',')
    end
  end
end

# /app/initializers/core_extensions.rb
require Rails.root.join('lib', 'core_extensions', 'comma_seperated')

# or to require all files in dir:
Dir.glob(Rails.root.join('lib', 'core_extensions', '*.rb')).each do |f|
  require f
end


Float.include CoreExtensions::CommaSeperated

Note that here we are not using the Rails autoloader at all and explicitly requiring the patch. Also note that we are placing the files in /lib not /app. Any files that are not application specific should be placed /lib.

Placing the monkey-patch in a module lets you test the code by including it in an arbitrary class.

class DummyFloat

  include CoreExtensions::CommaSeperated

  def initialize(value)
    @value = value
  end

  def to_s
    @value.to_s
  end
end

RSpec.describe CoreExtensions::CommaSeperated do
  subject { DummyFloat.new(1.01) }
  it "produces a comma seperated string" do
    expect(subject.comma_sep).to eq "1,01"
  end
end

This also provides a much better stacktrace and makes it much easier to turn the monkey patch off and on.

But in this case I would argue that you don't need it in the first place - Rails has plenty of helpers to humanize and localize numbers in ActionView::Helpers::NumberHelper. NumberHelper also correctly provides helper methods instead of monkeypatching a core Ruby class which is generally best avoided.

See:

max
  • 96,212
  • 14
  • 104
  • 165
  • Thanks a lot for a very thorough answer! – mityakoval May 28 '18 at 18:45
  • I also appreciate your very thorough answer, but some questions remain. I happened across an article containing the dire-sounding text "Review any monkeypatches as they will most likely need to be modified to work with the next version [6] of Rails" and I'm left wondering "modified how?" Hopefully @max' answer is the correct answer to how. See https://dev.to/hint/rails-6-upgrade-best-practices-5934 – Lori Jun 17 '19 at 21:16
  • @lori monkeypatches are always prone to breaking when you upgrade, and its very hard to predict exactly which ones will break. Thats why they are best left as a last resort. – max Jun 20 '19 at 18:28