I'm upgrading my Rails app from v5 to v6.
The files are structured as:
lib
|- container
|- my_module_a
| |- my_class.rb
| |- class_two.rb
|- my_module_b
my_module directory has no files in it, only my_module_a and my_module_b directories.
class_one.rb's contents:
module MyModuleA
class MyClass
# definitions...
end
end
config/application.rb has:
config.eager_load_paths += %W[
#{config.root}/lib/container
#{config.root}/lib/container/my_module_a
]
bin/rails zeitwerk:check prints All is good!
However, when running the rails server and it reaches the call of the MyClass class
obj = Container::MyModule::MyClass.new(...)
it prints
uninitialized constant Container::MyModuleA
If you want to call Container::MyModuleA::MyClassA, the class definition should be:
module Container
module MyModuleA
class MyClass
# definitions...
end
end
end
Your current directory structure and the class definition are different container/my_module_a/my_class vs MyModuleA::MyClass, the best practice is to prefix your classes with a module name that matches the directory tree. In development rails is disabling eager loading meaning that code is loaded on the fly (to reduce booting time) and when you call Container::MyModuleA::MyClassA, it will lookup for the container/my_module_a/my_class_a file, check the content to find Container::MyModuleA::MyClassA which is not defined right now are the class definition is not nested in the Container module.
More information about autoloading vs eager loading here: https://www.bigbinary.com/books/learn-rubyonrails-book/loading-behavior-of-ruby-on-rails-in-depth
I suspect the description above has multiple typos:
I don't see a my_module directory
Since lib/container/my_module_a is in the autoload paths, the my_class.rb file in that directory is expected to define a top-level MyClass class, not MyModuleA::MyClass as shown above
Yet, zeitwerk:check passes
Would be cool to have real names and real code to avoid assuming things. If not possible, at least double-check and update the problem statement.
The main thing to take into account is that autoload paths have to comply with the documented project structure conventions. Anything pushed to the eager load paths is considered to be an autoload path too.
You should remove the configuration for #{config.root}/lib/container/my_module_a unless you know what you are doing and really need nested root directories.
Then, if you leave #{config.root}/lib/container in the configuration, the code shown above is mostly good because this configuration says lib/container is a root directory containing top-level constants. But then, existing code should accordingly NOT use a Container constant nobody is defining.
So,
obj = Container::MyModule::MyClass.new(...)
does not square with what you have, should be
obj = MyModuleA::MyClass.new(...)
Please don't hesitate to follow up if more help is needed.
Related
I've multiple issues to load / require classes under my app/services folder in a Rails 5 project and I'm starting to give up on this issue.
First of all and to be clear, services/ are simple PORO classes I use throughout my project to abstract most of the business logic from the controllers, models, etc.
The tree looks like this
app/
services/
my_service/
base.rb
funny_name.rb
my_service.rb
models/
funny_name.rb
Failure #1
First, when I tried to use MyService.const_get('FunnyName') it got FunnyName from my models directory. It does not seem to have the same behavior when I do MyService::FunnyName directly though, in most of my tests and changes this was working fine, it's odd.
I realised Rails config.autoload_paths does not load things recursively ; it would makes sense that the first FunnyName to be catch is the models/funny_name.rb because it's definitely loaded but not the other.
That's ok, let's find a workaround. I added this to my application.rb :
config.autoload_paths += Dir[Rails.root.join('app', 'services', '**/')]
Which will add all the subdirectories of services into config.autoload_paths. Apparently it's not recommended to write things like that since Rails 5 ; but the idea does look right to me.
Failure #2
Now, when I start my application it crashes and output something like this
Unable to autoload constant Base, expected
/.../backend/app/services/my_service/base.rb to define it (LoadError)
Names were changed but it's the matching path from the tree I wrote previously
The thing is, base.rb is defined in the exact file the error leads me, which contains something like
class MyService
class Base
end
end
Poor solution
So I try other workaround, lots of them, nothing ever works. So I end up totally removing the autoload_paths and add this directly in the application.rb
Dir[Rails.root.join('app', 'services', '**', '*.rb')].each { |file| require file }
Now the base.rb is correctly loaded, the MyService.const_get('FunnyName') will actually return the correct class and everything works, but it's a disgusting workaround. Also, it has yet not been tested in production but it might create problems depending the environment.
Requiring the whole tree from the application.rb sounds like a bad idea and I don't think it can be kept this way.
What's the cleanest way to add custom services/ directory in Rails ? It contains multiple subdirectories and classes with simple names which are also present in other parts of the app (models, base.rb, etc.)
How do you avoid confusing the autoload_paths ? Is there something else I don't know which could do the trick ? Why did base.rb even crash here ?
Working solution
After deeper investigation and attempts, I realised that I had to eager_load the services to avoid getting wrong constants when calling meta functionalities such as const_get('MyClassWithModelName').
But here's is the thing : the classic eager_load_paths won't work because for some reason those classes will apparently be loaded before the entire core of Rails is initialized, and simple class names such as Base will actually be mixed up with the core, therefore make everything crash.
Some could say "then rename Base into something else" but should I change a class name wrapped into a namespace because Rails tell me to ? I don't think so. Class names should be kept simple, and what I do inside a custom namespace is no concern of Rails.
I had to think it through and write down my own hook of Rails configuration. We load the core and all its functionalities and then service/ recursively.
On a side note, it won't add any weight to the production environment, and it's very convenient for development.
Code to add
Place this in config/environment/development.rb and all other environment you want to eager load without Rails class conflicts (such as test.rb in my case)
# we eager load all services and subdirectories after Rails itself has been initializer
# why not use `eager_load_paths` or `autoload_paths` ? it makes conflict with the Rails core classes
# here we do eager them the same way but afterwards so it never crashes or has conflicts.
# see `initializers/after_eager_load_paths.rb` for more details
config.after_eager_load_paths = Dir[Rails.root.join('app', 'services', '**/')]
Then create a new file initializers/after_eager_load_paths.rb containing this
# this is a customized eager load system
# after Rails has been initialized and if the `after_eager_load_paths` contains something
# we will go through the directories recursively and eager load all ruby files
# this is to avoid constant mismatch on startup with `autoload_paths` or `eager_load_paths`
# it also prevent any autoload failure dû to deep recursive folders with subclasses
# which have similar name to top level constants.
Rails.application.configure do
if config.respond_to?(:after_eager_load_paths) && config.after_eager_load_paths.instance_of?(Array)
config.after_initialize do
config.after_eager_load_paths.each do |path|
Dir["#{path}/*.rb"].each { |file| require file }
end
end
end
end
Works like a charm. You can also change require by load if you need it.
When I do this (which is in all of my projects), it looks something like this:
app
|- services
| |- sub_service
| | |- service_base.rb
| | |- useful_service.rb
| |- service_base.rb
I put all common method definitions in app/services/service_base.rb:
app/services/service_base.rb
class ServiceBase
attr_accessor *%w(
args
).freeze
class < self
def call(args={})
new(args).call
end
end
def initialize(args)
#args = args
end
end
I put any methods common to the sub_services in app/services/sub_service/service_base.rb:
app/services/sub_service/service_base.rb
class SubService::ServiceBase < ServiceBase
def call
end
private
def a_subservice_method
end
end
And then any unique methods in useful_service:
app/services/sub_service/useful_service.rb
class SubService::UsefulService < SubService::ServiceBase
def call
a_subservice_method
a_useful_service_method
end
private
def a_useful_service_method
end
end
Then, I can do something like:
SubService::UsefulService.call(some: :args)
With your tree,
app/
services/
my_class/
base.rb
funny_name.rb
my_class.rb
models/
funny_name.rb
services/my_class/base.rb should look similar to:
module MyClass
class Base
services/my_class/funny_name.rb should look similar to:
module MyClass
class FunnyName
services/my_class.rb should look similar to:
class MyClass
models/funny_name.rb should look similar to:
class FunnyName
I say "should look similar to" because class/module are interchangable; Rails is merely looking for these constants to be defined in these locations.
You don't need to add anything to your autoload path. Rails automatically picks up everything in app
Anecdotal: With your services directory, it's fairly common to treat their naming convention (both name of file and underlying constant) to be "_service.rb" or "ThingService" — just like how controllers look. Models don't get this suffix because they're treated as first-class objects.
GitLab has some great file structure that is very worth a look at. https://gitlab.com/gitlab-org/gitlab-ce
So have a rails 5 project and would like to load a directory like this
/app
/services
/user
foo.rb
as the constant ::Services::User::Foo
Does anyone have experience in getting rails autoload paths to load the constants in this manner?
foo.rb
module Services
module User
class Foo
end
end
end
SOLUTION
Add this to your application.rb file
config.autoload_paths << Rails.root.join('app')
See discussions here on autoloading
https://github.com/rails/rails/issues/14382#issuecomment-37763348
https://github.com/trailblazer/trailblazer/issues/89#issuecomment-149367035
Auto loading
You need to define Services::User::Foo inside app/services/services/user/foo.rb
If you don't want this weird subfolder duplication, you could also move services to app/models/services or lib/services.
You could also leave foo.rb in app/services/user/foo.rb, but it should define User::Foo.
Eager loading
If you don't need any magic with namespaces and class names, it is pretty straightforward :
Dir[Rails.root.join('app/services/**/*.rb')].each{|rb| require rb}
This will eagerly load any Ruby script inside app/services and any subfolder.
Is there a way to tell Rails that all files in a certain folder are contained in a certain namespace?
Ie.
I have a file bar.rb in app/foo. Rails will assume this file defines Bar, but instead I want this file to define Foo::Bar.
I know I can achieve this by adding my root to Rails' autoload paths, but that isn't a real solution. Is there any other way I can tell Rails that all files within app/foo reside in the Foo namespace?
EDIT: File tree for clarification
app
assets
controllers
models
foo
bar.rb
quux.rb
I would like to be able to define Foo::Bar and Foo::Quux in respectively bar.rb and quux.rb, while also using Rails autoloading. Without having to resort to the tree structure as below:
app
assets
controllers
models
foo
foo
bar.rb
quux.rb
You can autoload files with namespaces corresponding to directories inside /app by adding /app to your autoload paths in your application config.
# config/application.rb
config.autoload_paths << "#{config.root}/app"
I'm not sure whether or not this is what the author of the question meant by
I know I can achieve this by adding my root to Rails' autoload paths, but that isn't a real solution.
But this is definitely a real solution and the correct one. Doing this is fine and not at all unusual in my experience.
There are only minor side effects which are the cost of adopting the inconsistent naming conventions you want. If you refer to an undefined constant that matches the name of another directory in the app you'll get a slightly different error message.
# Models::Foo => LoadError (Unable to autoload constant Models::Foo, expected /app/models/foo.rb to define it)
But if you have existing namespaces that match directories in app loading will still work fine. Here's what Rails is doing:
It takes the paths added to config.autoload_paths and adds them to the defaults (the directories under app: app/models, app/controllers, app/foo etc). Then when a constant is referenced that is not already loaded Rails proceeds through those paths, looking for paths that match the constants. So when you reference Foo::Bar it looks for app/models/foo/bar.rb, app/controllers/foo/bar.rb etc. until it finds a file that defined Foo::Bar. All we're doing is adding app/foo/bar.rb to that lookup.
Rails doesn't assume namespace, it assumes path to the source file depending on a namespace, so:
$ cat app/foo/bar.rb
module Foo
class Bar
def bar
puts "i'm here"
end
end
end
$ rails c
2.2.0 :007 > b=Foo::Bar.new
=> #<Foo::Bar:0x00000005272770>
2.2.0 :008 > b.bar
i'm here
=> nil
You can try adding the folder you Rail's autoload paths configuration
# config/application.rb
# Rails 4
config.autoload_paths << Rails.root.join("app", "foo")
# Rails 5+
config.eager_load_paths << Rails.root.join("app", "foo")
I'm having a problem with a module name and the folder structure.
I have a model defined as
module API
module RESTv2
class User
end
end
end
The folder structure looks like
models/api/restv2/user.rb
When trying to access the class, I get an uninitialized constant error. However, if I change the module name to REST and the folder to /rest, I don't get the error.
I assume the problem has to do with the naming of the folder, and I've tried all different combos of /rest_v_2, /rest_v2, /restv_2, etc.
Any suggestions?
Rails uses the 'underscore' method on a module or class name to try and figure out what file to load when it comes across a constant it doesn't know yet. When you run your module through this method, it doesn't seem to give the most intuitive result:
"RESTv2".underscore
# => "res_tv2"
I'm not sure why underscore makes this choice, but I bet renaming your module dir to the above would fix your issue (though I think I'd prefer just renaming it to "RestV2 or RESTV2 so the directory name is sane).
You'll need to configure Rails to autoload in the subdirectories of the app/model directory. Put this in your config/application.rb:
config.autoload_paths += Dir["#{config.root}/app/models/**/"]
Then you should be able to autoload those files.
Also, your likely filename will have to be app/model/api/res_tv2/user.rb, as Rails uses String.underscore to determine the filename. I'd just call it API::V2::User to avoid headaches, unless you have more than one type of API.
I seem to be having trouble with the naming conventions of the Lib Folder in Rails, and the error messages provided to me do not help. [For example, I have received a message saying that XXX::YYY::TextBox is expected to be defined xxx/yyy/text_box.rb, even though it clearly was defined there.] I think I'm getting the convention wrong.
Let's say I am working on YourModule::MyModule::MyClass. I clearly get that this file should be located in
lib/your_module/my_module/my_class.rb
But what should the actual file here look like? Which one of these (if either) are correct?
#your_module/my_module/my_class.rb
module YourModule
module MyModule
class MyClass
...
end
end
end
Or
#your_module/my_module/my_class.rb
class MyClass
...
end
In other words, do I need to nest the class inside of the module structure or not?
The lib folder has few conventions, as it is not autoloaded. So, how you organize the files is up to you, but you do have to name the classes correctly. Your first example is correct.
To get the files included you need to specify you want them in your application.rb file, see this example: Best way to load module/class from lib folder in Rails 3?
I would recommend making a folder just called lib/modules, since you probably won't have very many. Name the file my_class.rb. Then in application.rb you need:
config.autoload_paths += %W(#{config.root}/lib/modules)
That should take care of your issue.