Rebuild Rails - Ch.3 - Rails Automatic Loading

2017.05.15 - Kait Wang @ Rails Study Group

Rebuild Rails - Ch.3 - Rails Automatic Loading




Kait Wang - 2017.05.15 @ Rails Study Group

Who

Kait Wang a.k.a Sibevin Wang


What is Automatic Loading

  1. Create a new rails app
  2. Run rails server
  3. Change root to "homes#show" in routes
  4. # config/routes.rb
    root "homes#show"
  5. An exception occurs
  6. Started GET "/" for 172.18.0.1 at 2017-05-08 05:34:30 +0000
    Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.1, ::1, ...
    ActionController::RoutingError (uninitialized constant HomesController):
    activesupport (5.1.0) lib/active_support/inflector/methods.rb:269:in `const_get'

What is Automatic Loading

  1. Add the "Homes" controller and views
  2. # app/controllers/homes_controller.rb
    class HomesController < ApplicationController
    end
    <!-- app/views/homes/show.html.erb -->
    Hello world !!
  3. Everything is fine without restarting the rails server, Magic !!

What is Automatic Loading

It's not working in a regular ruby code, even the file is given ... (source code)

# homes_controller.rb
class HomesController
  def show
    p "The homes#show is called!!"
  end
end
# main.rb
homes_ctrl = HomesController.new
homes_ctrl.show
main.rb:1:in `<main>': uninitialized constant HomesController (NameError)

Load codes from other files

You need require the file

# main.rb
require './homes_controller'
homes_ctrl = HomesController.new
homes_ctrl.show
The homes#show is called!!

There are two ways to load codes: require v.s. load
See also: ruby的load與require是差在什麼地方?

Load codes from other files

But we DON'T require files in rails ...

What happened?

In Ruby

Started GET "/" for 172.18.0.1 at 2017-05-08 05:34:30 +0000
Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.1, ::1, ...
ActionController::RoutingError (uninitialized constant HomesController):
activesupport (5.1.0) lib/active_support/inflector/methods.rb:269:in `const_get'

When we create a class

class HomesController do ... end

It means

HomesController = Class.new do ... end

In Ruby

  1. When a new constant appears, ruby would try to get it with Object.const_get
  2. When a constant is not found, the Object.const_missing is called

How to do magic

The "Automatic Loading" tricks :tada:

  1. Take over the progress when a constant is missing
    => Overwrite "const_missing"
  2. Try to load codes from a file and get the constant again
    => Require the file and call "const_get" again

How to do magic

class Object
  def self.const_missing(c)
    if c == :HomesController
      require './homes_controller'
      const_get(c)
    else
      super
    end
  end
end

homes_ctrl = HomesController.new
homes_ctrl.show

How to do magic

Try to find "Xxx::YYYZzzController" in "app/controllers/xxx/y_y_y_zzz_controller.rb"

def self.const_missing(c)
  if c =~ /.*Controller\Z/
    file_name = c.to_s.gsub(/::/, '/').
      gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
      gsub(/([a-z\d])([A-Z])/,'\1_\2').
      tr("-", "_").
      downcase
    require "./app/controllers/#{file_name}"
    const_get(c)
  else
    # ...

How to do magic

Add the folder path to $LOAD_PATH, ruby would search these paths when we require files

$LOAD_PATH << File.join(File.dirname(__FILE__), "app", "controllers")

class Object
  def self.const_missing(c)
    if c =~ /.*Controller\Z/
      file_name = c.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
        gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
      require file_name
      const_get(c)
    else
      # ...

How to do magic

If we use a wrong controller name ...

# app/controllers/homes_controller.rb
class HomeController
  def show
    p "The homes#show is called!!"
  end
end
ruby-2.2.3/lib/ruby/2.2.0/rubygems.rb:1119:in `each': stack level too deep (SystemStackError)
from ruby-2.2.3/lib/ruby/2.2.0/rubygems.rb:1119:in `find_unresolved_default_spec'
from ruby-2.2.3/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:43:in `require'
from main.rb:11:in `const_missing'
from main.rb:12:in `const_get'
from main.rb:12:in `const_missing'
from main.rb:12:in `const_get'
from main.rb:12:in `const_missing'
from main.rb:12:in `const_get'

How to do magic

def self.const_missing(c)
  super if @is_reloaded
  if c =~ /.*Controller\Z/
    @is_reloaded = true
    file_name = c.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
      gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
    require file_name
    result = const_get(c)
    @is_reloaded = false
    return result
  else
    super
  end
end

Back to rails

The magic only works with a precondition ...

Convention over Configuration

The "HomesController" MUST be in "app/controllers/homes_controller.rb"

More details: Rails Guide - Autoloading and Reloading Constants

Back to rails

Tricks in rails

Not mentioned

  • Implement automatic loading in a rack app (See the book example)
  • Do automatic loading when files are changed or deleted (See the book Exercise Two)
  • The detail implementation in rails, ex: handle the nested namespace, autoload_paths, STI, ... (See Rails Guilde)

Q & A