I created a constant that includes my API key for an app I'm working on. It's working in the rails console, but I'm having a hard time getting this to work in my tests. I have to use Bing so I added the API like so:
# config/bing.yml
development:
secret: 1234
test:
secret: 5678
production:
secret: abcd
# config/initializers/bing.rb
BING_CONFIG = YAML.load_file("#{::Rails.root}/config/bing.yml")[::Rails.env]
How would I ensure that BING_CONFIG loads into rspec? Currently, it's working and loading fine in the my rails console, but when I run my specs, I get the following error:
uninitialized constant SearchEngine::BING_CONFIG
Is the BING_CONFIG = … string resides in your class/module SearchEngine? If yes, try to
require 'search_engine'
(or how the file containing that class is called) in top of your rspec file. If no, fix the problem with namespaces, e.g.:
SearchEngine::BING_CONFIG = …
In my understanding, as far as you configure such a initialization in config/initializers, it should be loaded even in rspec run. But it is still not working...
Since your error output showing SearchEngine name, I guess you've wrapped the initializer inside a module.
If that is the case, the SearchEngine module name should be introduced before the above assignment. But when you introduce the module name, to make Rails's autoloader happy, you must follow Rails' way of naming files (and class/module name) according to the namespace hierarchy. And it's depends on how SearchEngine is defined. How do you want to define the module name?
Related
I'm working on upgrading a large legacy Rails application from 5.2 to 6.0. In the process of fixing the autoload paths, I've run into an issue where Rails/Zeitwerk seem to be breaking their own rules about how the names of constants are defined in relation to their filenames. I can't share actual code from this application, but the situation is essentially this:
In config/application.rb:
config.autoload_paths << "#{config.root}/app/models/coupons"
In app/models/coupons/burgerfrenchfry_coupon.rb:
class BurgerfrenchfryCoupon << ApplicationRecord
end
When another class in the application references the BurgerfrenchfryCoupon class, a NameError is thrown with BurgerFrenchfryCoupon as a suggested classname (that class does not exist in the application). When I require the app/models/coupons/burgerfrenchfry_coupon path directly in the file referencing BurgerfrenchfryCoupon I get a Zeitwerk error: Zeitwerk::NameError: expected file /redacted/app/models/coupons/burgerfrenchfry_coupon.rb to define constant BurgerFrenchfryCoupon, but didn't
I've done a thorough search of the application to find anywhere where the expectation could have been customized and I've come up with nothing. Does anyone have any ideas about the follow:
Why this is happening?
Where or how an override on the constant name expectation might have been made?
How I can configure Rails to recognize that this constant should be defined in this file without changing all of the references to it in the application to BurgerFrenchfryCoupon?
The problem is that, for some reason, the autoloader's inflector is configured to camelize "burgerfrenchfry_coupon" as "BurgerFrenchfryCoupon". If using Active Support inflections (the default), there's some custom inflection rule somewhere that affects this.
You can fix this particular one without affecting the rest of the application by overriding this way:
# config/initializers/autoloading.rb
inflector = Rails.autoloaders.main.inflector
inflector.inflect("burgerfrenchfry_coupon" => "BurgerfrenchfryCoupon")
That sets a special mapping in the autoloader's inflector that ignores everything else.
The answer here ended up being a custom inflection that had been added to activesupport.
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'BurgerFrenchfry'
end
As this inflection was necessary for other parts of the application,
to fix the Zeitwerk error I added the file config/initializers/zeitwerk.rb with the following content:
Rails.autoloaders.each do |autoloader|
autoloader.inflector.inflect(
"burgerfrenchfry_coupon" => "BurgerfrenchfryCoupon"
)
end
Which overrides the inflection for this one file
I am successfully using 'Aws' (Amazon Web Services) SDK version 2 (NOT 'AWS' in capital letters as that is SDK version 1 namespace, I am using 'Aws' version 2) and the namespace constant 'Aws' is known at the rails controller level in code, but if I go one level deeper, say trying to use 'Aws' in a model created by the same controller, the 'Aws' namespace is not known. I get an undefined constant 'Aws' error.
My assumption is that Aws toolkit is initialized by including the gem in the Gemfile and it is there and all that appears to be correct. bundle check indicates all are resolved, and as I said it works in the Controller, but not in model class code.
I am circumventing this by injecting the Aws SDK into the models but I don't think this is the correct way, maybe it is??? I am new to Ruby and Rails so if you can give me advice or help me get setup so that the Aws toolkit is known globally as I had anticipated, I would appreciate your help.
Thanks in advance!
The answer was that the scope operator needed to be used normally. As a new Ruby user, I was not accustomed to constants not being available globally, therefore the explanation of why the scope operator was needed is somewhat explained by these references:
https://www.tutorialspoint.com/ruby/ruby_operators.htm
Ruby dot "." and double Colon "::" Operators:
You call a module method by preceding its name with the module's name and a period, and you reference a constant using the module name and two colons.
The :: is a unary operator that allows: constants, instance methods and class methods defined within a class or module, to be accessed from anywhere outside the class or module.
Remember: in Ruby, classes and methods may be considered constants too.
You need just to prefix the :: Const_name with an expression that returns the appropriate class or module object.
If no prefix expression is used, the main Object class is used by default.
This is a tricky one to explain. I have a module in another module namespace like so:
# app/models/points/calculator.rb
module Points
module Calculator
def self.included(base)
base.send(:include, CommonMethods)
base.send(:include, "Points::Calculator::#{base}Methods".constantize)
end
end
end
So then in other classes all I need to do is:
class User
include Points::Calculator
end
I've specified this directory in application.rb to be autoloadable...(even though i think rails recurses through models...)
config.autoload_paths += Dir[ Rails.root.join('app', 'models', "points") ]
In development env, everything works fine. When running tests(and production env), I get the following error:
Unable to autoload constant Points::Calculator, expected /Users/pete/work/recognize/app/models/points/calculator.rb to define it (LoadError)
I actually followed the advice here to fix the problem: Stop Rails from unloading a module in development mode by explicitly requiring calculator.rb in application.rb.
However, why is this happening??
I stuck some debug output in ActiveSupport's dependencies.rb file and noticed that this file is being required twice. The first time its required I can see that the constant is indeed loaded.
But the 2nd time its required the constant has been unloaded as far as Rails can tell, but when the actual require is called, ruby returns false because ruby knows its already required it. Then Rails throws the "unable to autoload constant" error because the constant still isn't present and ruby didn't "re-require" the file.
Can anyone shed light on why this might be happening?
Rails augments the constant lookup mechanism of ruby.
Constant lookup in Ruby:
Similar to method missing, a Module#constant-missing is invoked when a reference to a constant fails to be resolved. When we refer to a constant in a given lexical scope, that constant is searched for in:
Each entry in Module.nesting
Each entry in Module.nesting.first.ancestors
Each entry in Object.ancestors if Module.nesting.first is nil or a module.
When we refer to a constant, Ruby first attempts to find it according to this built-in lookup rules.
When ruby fails to find... rails kicks in, and using its own lookup convention and its knowledge about which constants have already been loaded (by ruby), Rails overrides Module#const_missing to load missing constants without the need for explicit require calls by the programmer.
Its own lookup convention?
Contrasting Ruby’s autoload (which requires the location of each autoloaded constant to be specified in advance) rails following a convention that maps constants to file names.
Points::Calculator # =>points/calculator.rb
Now for the constant Points::Calculator, rails searches this file path (ie 'points/calculator.rb') within the autoload paths, defined by the autoload_paths configuration.
In this case, rails searched for file path points/calculator in its autoloaded paths, but fails to find file and hence this error/warning is shown.
This answer is an abstract from this Urbanautomation blog.
Edit:
I wrote a blog about Zeitwerk, the new code reloader in Rails. Check it out at -> https://blog.bigbinary.com/2019/10/08/rails-6-introduces-new-code-loader-called-zeitwerk.html
If someone is having this issue in rails 6 which has zeitwerk autoloader,
Change ruby constant lookup back to classic in your application.rb
# config/application.rb
#...
config.autoloader = :classic
#...
Read more details here Rails Official Guides
Calculator should be a class to be autoloaded correctly
module Points
class Calculator
...
end
end
I have a custom initializer config/initializers/api/v1.rb, in which I set some global constants for use accross the application:
module Api
module V1
DATE_FORMAT = '%Y-%m-%d'
end
end
The first time I hit a controller that users Api::V1::DATE_FORMAT, everything works as expected. If I hit that path again, I get a NameError uninitialized constant Api::V1::DATE_FORMAT.
The Rails documentation for configuration explicitly says it should handle this case:
You can use subfolders to organize your initializers if you like,
because Rails will look into the whole file hierarchy from the
initializers folder on down.
When storing files in a custom directory (Eg: app/presenters/), how do you ensure that namespaced classes are loaded?
For example, if you have:
app/models/mega_menu.rb
app/presenters/catalog_presenter.rb
app/presenters/mega_menu/catalog_presenter.rb
Rails fails to load MegaMenu::CatalogPresenter:
CatalogPresenter.new
=> #<CatalogPresenter:0x85bca68 #_routes=nil>
MegaMenu::CatalogPresenter.new
(irb):3: warning: toplevel constant CatalogPresenter referenced by MegaMenu::CatalogPresenter
=> #<CatalogPresenter:0x85750a0 #_routes=nil>
I've created a sample Rails 3.2 app that reproduces this problem.
In config/application.rb, the app's configured to load files in app/presenters/.
I solved this issue by using a require statement in an initializer. I don't like it much but I liked the structure and class names of my application, they made sense so an initializer was my best solution. In the initializer try:
require File.join(Rails.root, "app", "presenters", "mega_menu", "catalog_presenter")
require File.join(Rails.root, "app", "presenters", "catalog_presenter")
This problem occurs because autoload relies on const_missing being called which won't happen in your case.
When ruby first encounters a reference to MegaMenu::CatalogPresenter, the mega_menu/catalog_presenter.rb file has not been included. Standard ruby behaviour causes it walks up the namespace tree (figure of speech) and it instead finds the top level reference CatalogPresenter as this HAS been included at this point.
Creating new toplevel constants inside classes raises this error. You want something more like this in catalog_presenter.rb:
class MegaMenu
class MegaMenu::CatalogPresenter
end
end