Rails 3 autoloading with models in nested modules - ruby-on-rails

We're in the process of making a major database change to our Rails application. In order to be able to interop with the existing code, my plan is to do all the work in module namespaces to keep them separate from the existing models. However, I'm running into Rails autoload problems.
My file structure is like:
app/
models/
entity/
new_thing.rb
old_thing.rb
Where new_think.rb contains something like
module Entity
class NewThing
end
end
and old_thing.rb contains something like
class OldThing
end
OldThing gets autoloaded fine, but I keep getting errors like this:
Expected app/models/entity/new_thing.rb to define NewThing
Is there a way I can get it to correctly expect entity/new_thing.rb to define Entity::NewThing?

Try:
In your old_thing.rb
class OldThing
Extend Entity
end
or
class OldThing
require "entity/new_thing"
end

Related

Autoload paths and nested services classes crash in Ruby

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

Rails modules as strict namespaces

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

Make Rails autoloading/reloading follow dynamic includes

Context
I want to add some admin specific code to all models via concerns that are automatically included. I'm using the naming convention MyModel => MyModelAdmin and they're in the standard Rails directory app/models/concerns/my_model_admin.rb. Then I can glob over all of them and do MyModel.include(MyModelAdmin).
Issue
Dynamic includes work fine, but when changing the concern in development Rails doesn't reload it properly. In fact the concern seems to get removed.
Reproduction
app/models/my_model.rb
class MyModel
end
app/models/concerns/my_model_admin.rb
module MyModelAdmin
extend ActiveSupport::Concern
def say
"moo"
end
end
config/initializers/.rb
MyModel.include(MyModelAdmin)
So far so good, MyModel.new.say == "moo".
But now change say to "baa" and you get NoMethodError undefined method 'say'.
Notes
I tried a number of things that didn't help:
require_dependency
Model.class_eval "include ModelAdministration"
config.autoload_paths
Using another explicit concern in ApplicationModel with an included hook that includes the specific concern in each model.
ActiveSupport.on_load only triggered on Base not each model.
Does this mean Rails can only autoload using static scope? I guess Rails sees the concern change, knows the model has it included, reloads the model but the static model definition doesn't have the include so the concern goes missing and stops being tracked. Is there a way to force Rails to track dynamically included modules?

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 not finding classes within modules [duplicate]

This question already has an answer here:
`ClassName.constants` returning empty array in Rails app
(1 answer)
Closed 7 years ago.
I know that Rails has an opinion on what the name of my classes and modules should be. As such, I have tried to cohere with it.
In Rails.root/lib/query_finder directory, I have the following structure:
/lib
/query_finder
/adapters
active_record.rb
mongoid.rb
base.rb
/strategies
base.rb
In base.rb, I named my class like so:
module QueryFinder
class Base
end
end
In adapters/base.rb, I named my class like so:
module QueryFinder
module Adapters
class Base
end
end
end
In adapters/mongoid.rb, I named my class like so:
module QueryFinder
module Adapters
class Mongoid
end
end
end
In adapters/active_record.rb, I named my class like:
module QueryFinder
module Adapters
class ActiveRecord
end
end
end
But Rails is unable to find the adapters. I try to grab all the constants:
> QueryFinder::Adapters.constants
=> []
And it's giving me an empty array. I also added the following to autoload path:
config.autoload_paths += Dir["#{config.root}/lib/**/"]
What might I be doing wrong?
I just want to make a note that I am able to reference the constants and classes like so:
QueryFinder::Adapters::Base
=> QueryFinder::Adapters::Base
The problem is when I use the constants method, it gives an empty array.
The problem is lazy loading. Rails, in production mode, is usually set up to do lazy loading. That means that the constants are not actually defined until you reference them: At that time it notices you're referencing an undefined constant, finds the file, and loads it.
If you turned off lazy loading, then you could rely upon .constants to tell you what was available. But performance would suffer.
Another workaround would be for you to explicitly load the required files.

Resources