How to define a nested controller to work with Rails autoloader? - ruby-on-rails

Say I have two standard rails resources defined as controllers, each with their own CRUD actions (index, show, etc...)
EmployeesController
Employees::AssignmentController
Directory looks like -
app/
controllers/
- employees_controller.rb
employees/
- assignment_controller.rb
I'm trying to figure out how to best define the second nested controller to work with Rails autoloader.
Option A - Namespaced Constant
If I do -
class Employees::AssignmentController < ApplicationController
...
end
I get the following error in my app log and when running integration tests:
LoadError: Unable to autoload constant Employees::AssignmentController, expected /Users/jeeves/git/my-project/app/controllers/employees/assignment_controller.rb to define it>
It doesn't like the constant defined as one single namespace - Employees::AssignmentController
Option B - Nested Constant inside a module
If I do -
module Employees
class AssignmentController < ApplicationController
end
end
I actually get the same LoadError error as above.
Option C - Nested Constant inside a class
Finally, if I do -
class Employees
class AssignmentController < ApplicationController
end
end
I get TypeError: Employees is not a class, which makes sense since the outer class isn't defined. I try and fix it by adding an explicit class definition with an empty file
app/
controllers/
- employees_controller.rb
+ - employees.rb
employees/
- assignment_controller.rb
class Employees
end
And this works. But it feels silly to have this extra dummy file hanging around in app/controllers, especially when it's not even a controller itself.
Question
Is there a correct way to approach the above situation? Is Option C my only option? Am I even correctly using namespaces or should I avoid them all together?
Thanks!

Define (and reopen) namespaced classes and modules using explicit
nesting. Using the scope resolution operator can lead to surprising
constant lookups due to Ruby’s lexical scoping, which depends on the
module nesting at the point of definition.
- The Ruby Style Guide
An example of this suprising behavior is:
class Employees::AssignmentController
def index
#rate = Rate.find(...)
end
end
You would expect Rate to be resolved as Employees::Rate but its actually resolved to ::Rate since the lexical scope is ::.
This is the preferred method:
module Employees
class AssignmentController
def index
#rate = Rate.find(...) # resolves to Employees::Rate
end
end
end
It reopens the module and defines a new constant in the correct lexical scope.
As to alternative C - its possible since classes in Ruby are modules (check Class.ancestors if you don't belive me) but its not considered good form since classes should be things that can be instantiated.
The rails generators do this the "wrong" way by using the scope resolution operator but thats really just due to limitations in the file templating system.
Then why am I getting a load error?
I don't actually know. This is the preferred method and there is no obvious reason why its not working in this case. Double check everything. Restart spring by running spring stop and restart the server.

Related

How to namespace in ruby

I've got a rails model called LeadSource
class LeadSource < ActiveRecord::Base
end
I also have in the lib directory, the following structure:
-lib
-reports
base.rb
lead_source.rb
in the lead_source.rb, i've got this defined:
module Reports
class LeadSource < Reports::Base
end
end
When I call LeadSource.new in the console, I get the following error:
LoadError: Unable to autoload constant LeadSource, expected ..../lib/reports/lead_source.rb to define it
I am under the assumption that the module Reports should be namespacing my LeadSource class. (allowing me to have two classes named the same thing). But why in this case, when I try to instantiate a new LeadSource object (active record model) is rails looking in lib and throwing a fit about my class defined there?
I would expect to call the active record model like this:
LeadSource.new
and the class in my lib like this:
Reports::LeadSource.new
without them colliding and causing issues.
EDIT:
I feel like this defeats the purpose of namespacing if the solution is to just change the class name as pointed out here as well as the similar question marked as duplicate of this one.
Am I wrong in thinking that a namespace should silo off the resource and not just make it so i have to type more to call it? seems silly to me.

Rails, Ruby classes / modules / namespaces confusion

I have a Rails model: app/models/A.rb -> class A < ActiveRecord::Base
Then I have: app/services/a/b/c.rb -> class A::B::C, where both a and b are folders. AFAIK, app/<foldername> is autoloaded, so that is not an issue.
And, finally, I have an module: module B (it is an external gem with this name).
If I attempt to do this: A::B::C.new it throws an error message: toplevel constant ... referenced by ... (Don't remember exactly) - in other words, it does NOT work.
Other classes defined under a folder - work just fine. For example: app/services/a/x.rb (class A::X)
If I try A::B, I simply get to B (the gem), with a warning message (but it continues to work).
But, if I rename either a or b folder name (and class signatures of course) - it works just fine!
So,
1) Why is that? How does ruby handle all these classes, modules, namespaces, inclusions?
2) Is it possible to have everything working w/o renaming a single thing?
Thanks in advance!
You may need an intermediate A::B declared in a/b.rb to properly define the module A::B before you reference it, or you can re-style your declaration in your final file.
For example:
# a/b/c.rb
class A::B::C
end
This depends on module or class A and A::B being defined before that file is loaded.
You can side-step this:
class A
module B
class C
end
end
end
That will force-declare all the intermediates. As this introduces a lot of indentation, a slightly less messy approach is:
class A
module B
end
end
class A::B::C
end
Of course, having an a.rb and a/b.rb with the appropriate intermediate declarations often helps avoid all of this.

Rails loading of non-ActiveRecord models

I have a Rails 3 app that defines a few non-ActiveRecord models. All of these are defined in app/models/module_name. I have the following in all_autoload_paths:
[4] pry(main)> app._all_autoload_paths
=> ["/Users/mandar/myapp/lib",
"/Users/mandar/myapp/app/assets",
"/Users/mandar/myapp/app/controllers",
"/Users/mandar/myapp/app/helpers",
"/Users/mandar/myapp/app/models"]
Is there a difference between how the following 2 class definitions are handled when loading models?
module A
class X
# some code
end
end
and
class A::Y
# some code
end
The reason I ask this is sometimes I've seen a uninitialized constant A::Y - NameError error.
Update: Following Sibevin's answer, I'd like to make this clearer. Currently, I have the following file structure:
app
- models
- a
- x.rb
- y.rb
I have seen the error for Y which uses the ModuleName::ClassName syntax, but almost never for X.
Thanks for the help!
They should be no difference, but I usually separate them into individual files.
You can create a folder app/models/a/ first and put your class A::X in app/models/a/x.rb.
Similarly, app/models/a/y.rb for class A::Y.
BTW, app/models/a.rb for your module A if needed.
UPDATE:
Actually, I never use the 2nd format to declare a class in a module. Maybe the following doc can answer your question:
Everything you ever wanted to know about constant lookup in Ruby
If you've ever tried to take a short-cut when re-opening a module, you
may have noticed that constants from skipped namespaces aren't
available. This is because the outer namespaces are not added to
Module.nesting.

Rails class loading skips namespaced class when another class of same name in root namespace is loaded

I have two namespaces, each with its own controller and presenter classes:
Member::DocumentsController
Member::DocumentPresenter
Guest::DocumentsController
Guest::DocumentPresenter
Both presenters inherit from ::DocumentPresenter.
Controllers access their respective presenters without namespace specified, e.g.:
class Guest::DocumentsController < ActionController::Base
def show
DocumentPresenter.new(find_document)
end
end
This usually calls presenter within same namespace. However sometimes in development environment I see base ::DocumentPresenter is being used.
I suspect the cause is that base ::DocumentPresenter is already loaded, so Rails class auto-loading doesn't bother to look further. Is this likely the case? Can it happen in production environment too?
I can think of two solutions:
rename base class to DocumentPresenterBase
explicitly require appropriate presenter files in controller files
Is there a better solution?
You are correct in your assumptions - If you do not specify namespace, Ruby starts from current namespace and works its way up to find the class, and because the namespaced class is not autoloaded yet, the ::DocumentPresenter is found and autoloader does not trigger.
As a solution I would recommend renaming ::DocumentPresenter to DocumentPresenterBase, because this protects you from bugs when you forget namespacing or explicit requiring somewhere.
The second option to consider would actually be using specific namespaced classnames all over the place, but this suffers from bugs when you accidentally forget to namespace some call.
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
Third option would be your second - explicitly require all the classes in initializer beforehand. I have done this with Rails API which receives embedded models in JSON and Rails tends to namespace them when the actual models are not loaded yet.
Option 3.5 You could probably trick autoloader to do the heavy lifting (though, this might seem more like a hack):
class Guest::DocumentsController < ActionController::Base
# trigger autoload
Guest::DocumentPresenter
def show
# This should refer Guest::DocumentPresenter
DocumentPresenter.new(find_document)
end
def show
# As will this
DocumentPresenter.new(find_document)
end
end
Still the cleanest would be to rename the base class.
I think in 3 solutions if you want to mantein the name, one is your second solution.
1) explicitly require appropriate presenter files in controller files
2) Execute the full environment class path, like:
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
3) Create a file on initialize directory and execute require manually (the worst options :S)

Uninitialized constant trying to reopen a Class

I am using a plugin in Rails, and I call its methods without problems:
plugin_module::class_inside_module.method_a(...)
I want to re-open the class_inside_module and add a new method, I tried in many different ways. I can't figure out why in this way doesn't work:
class plugin_module::class_inside_module
def new_method
puts 'new method'
end
end
I get the error: uninitialized constant plugin_module, but how is possible if I can call without problem plugin_module::class_inside_module.any_methods ?
Do you know why I get that error ? why "uninitialized constant" ? (it is a class declaration :-O )
Do you have any ideas how I can add a new methods in a class inside a module (that is part of a plugin) ?
Thank you,
Alessandro
If you have written your class and module-names like you did, so plugin_module instead of PluginModule this is against ruby/rails standards, and rails will not be able to automatically find the class and module.
If you write something like
module MyModule
class MyClass
end
end
Rails will expect this file to be located in lib\my_module\my_class.
But this can always easily be overwritten by explicitly doing a require.
So in your case, when you write
module plugin_module::class_inside_module
Rails will not know where to find the module plugin_module.
This way of writing only works if module plugin_module is previously defined (and loaded).
So either add the correct require, or rename your modules to standard rails naming, or write it as follows:
module plugin_module
class class_inside_module
This way will also work, because now the order no longer matters.
If the module is not known yet, this will define the module as well.
Either you are re-opening the class, or you define it first (and the actual definition will actually reopen it).
Hope this helps.
Have you tried reopening the module that's wrapping the class, rather than relying on ::?
module plugin_module
class class_inside_module
def new_method
puts 'new_method'
end
end
end
By the way, you know that the proper name for modules and classes is use CamelCase with a capital first letter?
module PluginModule
class ClassInsideModule
def new_method
puts 'new_method'
end
end
end

Resources