I'm quite new to rails and I'm a bit confused of how modules work here. I have a project structure like this:
# app/models/foo.rb
class Foo < ActiveRecord
# lib/external_service/foo.rb
module ExternalService
class Foo
# lib/external_service/bar.rb
module ExternalService
class Bar
attribute :foo, Foo # not the model
I have worked with many coding languages before and I expected it to be easily possible to use 'Foo' inside Bar and ExternalService just like that but
LoadError: Unable to autoload constant Foo, expected lib/external_service/foo.rb to define it
The ExternalService::Foo should normally not even be visible outside of ExternalService but the whole project dies on this thing
Am I just missing a kinda 'strict mode'-notation or anything to make sure that I obviously mean ExternalService::Foo inside the service and prevent the service from killing my model?
I know I can just prepend the module but i wanna keep the code readable.
so you are using rails 4
if you want to create a module, first you need to import or autoload your lib folder
for example in application.rb you can add lib folder to autoload:
config.autoload_paths << Rails.root.join('lib')
after that because you are using rails you should create a folder hierarchy with snake cased name of your module hierarchy
for example if you have:
module ExternalService
class Foo
...
end
end
your foo.rb file should be in a folder with name 'external_service'
{{project_root}}/lib/external_service/foo.rb
folder hierarchy is convention of rails.
Ruby behaves just like this and it's totally ok.
In this case the Foo-Model is already loaded, so ruby prefers this instead of the local one. Also alphabetically app/ is before lib/
A not so beautiful but quick fix is just to call it like this:
attribute :foo, ExternalService::Foo
Related
I am trying to add custom folder named foo inside app directory like this app/foo. But rails is not able to find module. This is just a example class inside app/foo/bar.rb
module Foo
class Bar
end
end
so if I try to do Foo::Bar.new in console I get uninitialized constant Foo (NameError). But if i remove the namespace from the bar.rb file then it works. e.g this will work
class Bar
end
Bar.new works fine is rails console, if i remove namespace from the bar.rb file(NOTE: bar.rb file is still under app/foo/bar.rb. I also tried adding config.autoload_paths << "#{root}/app/foo" in application.rb file but no luck.
If I put foo directly under api directory like this app/api/foo/bar.rb then it works fine. If I also add foo directly under models directory then it works fine e.g app/models/foo/bar.rb
Whats going on? How can I add foo directory under app directory ?
ActiveSupport::Dependencies.autoload_paths shows the directory foo is getting loaded fine.
UPDATE
Looks like what I am trying to achieve can't be done without putting the foo directory inside some other directory. But it makes sense because if we have user.rb file inside app/models/user.rb we don't do class Models::User we do class User. so make sense.
You need to add the following line to application.rb file in order to configure autoloading.
config.autoload_paths << "#{root}/foo"
for more about this you can read at https://guides.rubyonrails.org/autoloading_and_reloading_constants.html
UPDATE
Anything under app directory will be autoloaded by default.
for app/foo/bar.rb
content should be.
class Bar
end
if you need to make it something like this.
module Foo
class Bar
end
end
then your directory structure should be app/custom_folder_name/foo/bar.rb
I will recommend to use custom_folder_name as extras
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
I'm wanting to create a custom directory under app, and have a corresponding namespace, like:
app/my_widgets
and
#app/my_widgets/foo
module MyWidgets
class Foo
#...
end
end
This doesn't work.
LoadError: Unable to autoload constant Foo, expected /project_dir/app/my_widgets/foo.rb to define it.
I am calling:
MyWidgets::Foo.new()
I had guessed this could be because rails autoloads constants from subdirectories of app/... However, adding:
config.autoload_paths += %W{#{config.root}/app"}
Doesn't fix it as I thought it might. Is this a lost cause?
I have tried moving my_widgets/ to something else, eg 'services/' under app, and then everything works as expected.
Is there any way to have a namespace & corresponding directory directly under app/, though?
My application code is structured as shown below in Rails 3.2. If I go into the Rails console and enter Foo::Bar::FooBar it will return this warning:
warning: toplevel constant FooBar referenced by Foo::Bar::FooBar
Application code and files they are located in:
# app/models/foo/bar/foo_bar.rb
module Foo
class Bar
class FooBar
end
end
end
# app/models/foo/bar.rb
module Foo
class Bar
end
end
# app/models/foo_bar.rb
class FooBar
end
My autoload paths have not been changed from the Rails defaults.
One way I've been able to fix the issue has been to add the following code to Foo::Bar::FooBar. However, it feels dirty and I was wondering if there was a configuration option or some other thing I'm doing wrong that would fix the issue.
# app/models/foo/bar/foo_bar.rb
module Foo
# This line of code removes the warning and makes class methods execute
# on the Foo::Bar::FooBar class instead of the FooBar class.
class Bar; end
class Bar
class FooBar
end
end
end
The basic problem is that you're reusing the same name for different classes, in an overlapping scope. You might be able to do something clever to make Ruby resolve the constant name in the way you want, but this kind of "solution" is fundamentally brittle. (What if a new version of Ruby makes a tiny change to how constants are looked up, or you move to another Ruby implementation?)
Why don't you just namespace the top-level FooBar model class within a Module? (You'll have to move the file into a subdirectory with the same name as the Module.)
In my rails app I have a models/foo.rb and models/foo/exceptions.rb:
class Foo
include Foo::Exceptions
end
module Foo::Exceptions
class MySpecialException < Exception
end
end
At the rails console, I then see this:
>> Foo::MySpecialException
Foo::Exceptions::MySpecialException < Exception
>> Foo::MySpecialException == Foo::Exceptions::MySpecialException
true
>> Foo::MySpecialException === Foo::Exceptions::MySpecialException
false
What do you think of my file structure / namespacing?
Is my inclusion line in Foo necessary, or does Rails autoinclude those modules?
Why does MySpecialException exist in the top level Foo namespace and point to Foo::Exceptions::MySpecialException?
What does it mean that those two classes are == but not ===?
I explored this in irb but kept running into errors that were inconsistent with the behavior I (think) I've seen in Rails.
related: What is a conventional place to keep custom Exception definitions in a rails project?
What is the relationship between a ruby class and module with the same name?
A class and a module can't have the same fully qualified name in ruby. It's possible to have a class Foo::Bar and a module Baz::Bar, in which case there is no relation between the class and the module. However it is not possible to both have class Foo::Bar and a module Foo::Bar at the same time.
(I'm not sure what this has to do with the rest of your question though)
Is my inclusion line in Foo necessary, or does Rails autoinclude those modules?
Rails will not automatically include your modules. However that does not mean it's necessary to include yourself, you could just access it qualified. I.e. use Exceptions::MySpecialException instead of just MySpecialException inside the Foo class.
Why does MySpecialException exist in the top level Foo namespace and point to Foo::Exceptions::MySpecialException?
Because you included Foo::Exceptions into Foo. Because of that all instance methods of Foo::Exceptions are also instance methods of Foo and all constants of Foo::Exceptions are also constants of Foo - including MySpecialException.
What does it mean that those two classes are == but not ===?
== means that it's the same class. That it's not === means that the class is not an instance of itself (since x === y is the same as y.is_a?(x) if x is a class).
I suggest that you move exceptions to lib: lib/exceptions/exception_name.rb
Rails will auto load stuff in models dir, however for the lib folder, you gotta tell to do so by checking config/application.rb and adding this:
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]