I have a concern with realoding of constant in rails. More specifically, with a constant that is filled by an inherited method :
EDIT : This concerns is relative to development mode, not production.
Here is the code :
# helpers/my_active_code/formatter.rb
module MyActiveCode
class Formatter
FORMATTER_MAPPING = ActiveSupport::HashWithIndifferentAccess.new # just a constant
# Will store something like :
# {
# "String" => ::MyActiveCode::Transforms::String,
# "Boolean" => ::MyActiveCode::Transforms::Boolean,
# }
end
end
# helpers/my_active_code/transforms/object.rb
module MyActiveCode::Transforms
class Object
# A superclass that implement an inherited method which store into the constant
private
def self.inherited(subclass)
ActiveRecordViewRewrite::Formatter::FORMATTER_MAPPING.store(identifier, subclass)
end
end
end
# helpers/my_active_code/transforms/string.rb
module MyActiveCode::Transforms
class String
end
end
I also got an initializer that require all files into the folder helpers/my_active_code/transforms.
When i start the rails console, all work fine, the FORMATTER_MAPPING constant is well filled.
However, after a reload (reload!) the FORMATTER_MAPPING constant is empty ({}).
This is perfectly normal due to the constant-reloading describe here : http://guides.rubyonrails.org/autoloading_and_reloading_constants.html#constant-reloading
So i try to reload all my transforms file into (helpers/my_active_code/transforms) at the end of the file helpers/my_active_code/formatter.rb but it does not work.
How can I solve this and always get this constant filled?
Related
I am getting `const_missing': uninitialized constant Disc::Foo1 (NameError) while executing the code
disc.rb
module Disc
MAPPING = {
:new => {
:first => Foo1,
:second => Foo2
},
:old => {
:third => Foo3,
:fourth => Foo4
}
def run
# :new and :first is example but will be any value decided based on the logic dynamically.
cls = MAPPING[:new][:first]
cls.new.execute
end
}
end
foo1.rb
class Foo1
def initialize
end
def execute
puts "f1"
end
end
foo2.rb
class Foo2
def initialize
end
def execute
puts "f2"
end
end
foo3.rb
class Foo3
def initialize
end
def execute
puts "f3"
end
end
foo4.rb
class Foo4
def initialize
end
def execute
puts "f4"
end
end
I am new to ruby. I tried to find the solution and found that I have to add require clause and tried many things but didn't work.
Can someone please help me out here?
Ruby can't autoload files — you need to explicitly tell it to load them. Just by sitting in the same directory they will not do anything. This is because, unlike some other languages, there is no predictable set mapping between file names and their contents, so Ruby can't know where the class you wanted is. You have to
require './foo1'
require './foo2'
require './foo3'
require './foo4'
Alternately, you could do it a bit smarter:
%w(./foo1 ./foo2 ./foo3 ./foo4).each do |file|
require file
end
Now that you know about require... Saying that Ruby can't autoload files was a lie to tell people new to Ruby. Ruby can totally autoload files. But you still have to tell it what they are and where to find them. Just by sitting in the same directory they will not do anything. You have to
autoload(:Foo1, './foo1.rb')
autoload(:Foo2, './foo2.rb')
autoload(:Foo3, './foo3.rb')
autoload(:Foo4, './foo4.rb')
The files will not be loaded, unless you try to access the named classes; when you do so, Ruby will remember you told it where to find those classes, and load the corresponding file. This saves the program start time and memory, as only the actually needed files will be loaded.
You can also do this instead:
# for each `./foo*.rb` file
Dir.glob('./foo*.rb').each do |file|
# turn it into the class name; e.g. `./foo1.rb' -> :Foo1
classname = file[%r{\./(.*)\.rb$}, 1].capitalize.to_sym
# then tell Ruby that that classname can be found in that file
autoload(classname, file)
end
However, since you explicitly have the classes in your MAPPING, autoloading will not help you — all of the foo files will be autoloaded at the moment MAPPING is defined, so there is no benefit to using autoload here. If you stored names instead of class objects themselves, and used Object.const_get to look up the class object constant, then it would work:
autoload(:Foo1, './foo1.rb')
# `./foo1.rb` is not yet loaded
MAPPING = {
new: {
first: :Foo1,
# ...
},
# ...
}
# `./foo1.rb` is still not loaded:
# we haven't used the `Foo1` constant yet
# (we did use `:Foo1` symbol, but that is a completely different thing)
cls = Object.const_get(MAPPING[:new][:first])
# `./foo1.rb` is now loaded, and `cls` is `Foo1`
On this project setup :
RoR : 6.0.3.4
Ruby: 2.7.1p83
I have this bit of code serving as a Nokogiri helper - within app > helpers > nepper > helpers > nokogiri.rb (a bit redundant fo'sure) :
# frozen_string_literal: true
module Nepper
module Helpers
# Dedicated to Nokogiri utilities
module Nokogiri
# HTML::Document extension
class ::Nokogiri::HTML::Document
def has?(css_selector)
# css_selector (symbole): const name
css(css_selectors.const_get(css_selector)).present?
end
def text_at(css_selector)
# css_selector (symbole): const name
elt = css(css_selectors.const_get(css_selector))
# raise TypeError 'No text on node' unless elt.text.present?
elt.text
end
def href_at(css_selector)
# css_selector (symbole): const name
elt = css(css_selectors.const_get(css_selector)).first
# raise TypeError 'Not a link' unless elt[:href].present?
elt[:href]
end
end
# css selectors constants shortcut
def css_selectors
Nepper::Constants::CssSelector
end
end
end
end
and within a model declaration, something looking like this
class Card
include Mongoid::Document
include Nepper::Helpers::Nokogiri
# [...]
end
My issue is that, within rails console, I get undefined method for Nokogiri::HTML::Document error for css_selectors whenever an instance of the class is using any of the previous added methods.
More troublesome : It worked just fine yesterday right before pushing the code.
I could put the method within the Nokogiri::HTML::Document method implementation, but I want to keep this bit of code accessible whenether a file includes it, without having to use a Nokogiri instance nor duplicating the code.
So ... What's going on with this weird load behaviour ?
I don't get why the Nokogiri::HTML::Document instances don't access the module method anymore ... both should exist once this code is included isn't it ?
Edit : Btw I had the same issue with TypeError being errored as undefined - for why I had to comment those raise error parts (also weird). This is as if some fo the Main object modules are not loaded prior to those methods calls.
Disclaimer : I know that it's a very bad pattern, I just want to understand why I have the following behaviour and the mechanics behind it.
Lets have one class with some constants / methods
one concern
one monkey patch that include that concern into my first class
I put all of the following in one rails model (under app/modela/a.rb)
class A
MY_CONST = "const"
def self.hello
"world"
end
end
module MyConcern
extend ActiveSupport::Concern
included do
byebug # Here I want to try many things
end
end
class A
include MyConcern
end
Then I open my console and run the following to load my class :
[1] pry(main) > A
First I'm my A Class is loaded, then the MyConcern module, then the monkey patch.
When I enter my byebug I have some weird behaviour
(byebug) self
A # Important : I'm in the scope of my A class
(byebug) hello
"world" # so far so good, my A class is loaded and have a world class method
(byebug) A::MY_CONST
"const" # Obiously this is working
(byebug) self::MY_CONST
"const" # When I Explicitly access it through `self`, it's also working
(byebug) MY_CONST
*** NameError Exception: uninitialized constant MyConcern::MY_CONST
nil # Here for some reason, and that's what I want to understand, I can't access my const, even if `self == A`
I want' to know why constant are not working the same way. Why ruby isn't able to find this const. My scope is my A class, self == A, so why can't I access my constant?
ps : Rails.version => "6.0.0"
Having this piece of code in test1.rb
my_var = 42
def my_func()
42
end
class MyCLS
attr_accessor :prop
def initialize()
#prop = 42
end
end
Then in the interpreter I require it in irb
> require './test1.rb'
> MyCLS.new().prop
=> 42
> my_func()
=> 42
> my_var
NameError: undefined local variable or method `my_var' for main:Object
I am confused, ruby seems quite happy to pollute the global namespace with classes and functions, but refuse to do the same with my_var? I imagine this is to avoid name collisions and bugs. But the problem is only partially solved as it is still present with Class and Function. Maybe just a little less prone to happen?
So now imagine this second file test2.rb
def my_func()
43
end
class MyCLS
attr_accessor :prop
def initialize()
#prop = 43
end
end
And then execute it
> require './test1.rb'
> require './test2.rb'
> MyCLS.new().prop
=> 43
> my_func()
=> 43
Is that normal that the previous globals MyCLS and my_func get silently overwritten? Isn't this highly likely to break a software because a gem decided to add/rename a Class or a function somewhere? All of this seems very brittle and dangerous.
I am aware of modules and I tried them with little success (awkward, and once again they are globals)
Is there ways to prevent this or mitigate what seems like a language design flaw?
Edit: Another example
# test1.rb
def my_func()
42
end
# test2.rb
puts my_func()
# test3.rb
class Example
require './test1.rb'
end
class AnotherExample
require './test2.rb'
end
# command line
$ ruby test3.rb
42
Constants in Ruby (things which start with an uppercase letter) are always created as constants on Object, unless they are explicitly a member of another constant (ie, module Foo::Bar creates a module under the Foo constant, which is itself under the Object constant).
Additionally, there is a special top-level Object instance named "main". Any methods defined at the top level are defined on Object as private methods, and thus accessible from main.
When you require a file, the mechanism is:
Create a new anonymous Module
Load the requested file into that module
Extend main with that module.
These rules are always obeyed; you can't define a top-level method in a file, then include that file into a namespace by cleverly placing the require statement. The file is parsed, Ruby finds a top-level method and extends main with it, without any consideration for where require is called from.
If you want a method that gets mixed into another class, then you typically would put it into a module, and then mix that module into your class.
# test1.rb
module Bar
def my_func
42
end
end
# test2.rb
require 'test1'
class Foo
include Bar
end
my_func => # NameError: undefined local variable or method `my_func' for main:Object
Foo.new.my_func # => 42
In Ruby, it is expected that each file will fully namespace the constants and methods that it intends to expose. You will practically never write top-level methods in most real Ruby projects; there's little fear of things being unintentionally overwritten, because someone would need to explicitly step into your namespace to overwrite things.
If you want to execute a file without extending the main object, then you can use Kernel#load with the wrap parameter, which wraps the load in an anonymous module (but makes its internals inaccessible, unless you were do do something in that file to expose the methods and constants in that file):
load "test1", true
MyCLS # => NameError: uninitialized constant MyCLS
You could get this scoped kind of loading via a custom loader:
# test1.rb
def foo
42
end
# test2.rb
def relative_load(file)
Module.new.tap {|m| m.module_eval open(file).read }
end
class Foo
include relative_load("test1.rb")
end
Foo.new.foo # => 42
foo # => NameError: undefined local variable or method `foo' for main:Object
As an aside, in your first example, the MyCLS class isn't overwritten; it's merged with the existing MyCLS class. Because both declare initialize, the latter declaration takes precedence. For example:
# test1.rb
class MyCLS
attr_accessor :prop
# This definition will get thrown away when we overwrite it from test2.
# It is test2's responsibility to make sure that behavior is preserved;
# this can be done with reimplementation, or by saving a copy of this
# method with `alias` or similar and invoking it.
def initialize(prop)
#prop = prop
end
end
# test2.rb
class MyCLS
attr_accessor :another_prop
def initialize(prop, another_prop)
#prop = prop
#another_prop = prop
end
end
# test3.rb
require 'test1'
c = MyCLS.new(1, 2) # => ArgumentError: wrong number of arguments (2 for 1)
c = MyCLS.new(1)
c.prop => 1
c.another_prop => # => NoMethodError: undefined method `another_prop'
require 'test2'
c = MyCLS.new(1) # => ArgumentError: wrong number of arguments (1 for 2)
c = MyCLS.new(1, 2)
c.prop => 1
c.another_prop => 2
Since you are using the global namespace, I am going to venture that you are diving into Ruby with a background in JavaScript, correct me if I am wrong.
Ruby is not polluting the global namespace because when you require a file such as test_1.rb or test_2.rb, their scope is then limited to the place where you required them. For instance, if you require 'test_1' in a Class called Example, that has no reach if you were to require 'test_2' in a Class called AnotherExample:
Class Example
require 'test_1'
end
Class AnotherExample
require 'test_2'
end
Your methods get overwritten because you required both files within the same scope, which you would not do within the context of a larger application. Namespaces prevent you from overriding similarly named variables, methods, etc.
my_var is a local variable whose context is bound to test_1.rb. As such, its scope is limited to within test_1.rb.
In my Rails 3.2 app I have a bunch of plain old ruby objects in the /app/models/ directory. I'd like to move some of these into a separate folder, say /app/models/data_presenter/. For one of the objects,
# /app/models/data_presenter.rb
class DataPresenter
# ...
end
I've tried the following
# /app/models/data_presenter/data_presenter.rb
class DataPresenter::DataPresenter
# ...
end
however, I got the TypeError (wrong argument type Module (expected Class)) error. Any suggestions to overcome this (with or without namespaces)? Do I also need to change the corresponding models' tests names and locations?
As #BroiSatse pointed out, the problem was that I had a bunch of subclasses that were inheriting from the base class DataPresenter. For those subclasses I forgot about the namespacing, i.e.
# /app/models/data_presenter/color_data_presenter.rb
class ColorDataPresenter < DataPresenter
# ...
end
should have been
# /app/models/data_presenter/color_data_presenter.rb
class DataPresenter::ColorDataPresenter < DataPresenter::DataPresenter
# ...
end
or similarly
module DataPresenter
class ColorDataPresenter < DataPresenter
# ...
end
end
For the tests, I couldn't find a magick solution so I just wrote
# /test/unit/answers_presenter/color_data_presenter_test.rb
require 'test_helper'
class ColorDataPresenterTest < ActiveSupport:TestCase
should 'do something cool' do
presenter = DataPresenter::ColorDataPresenter.new
assert presenter.do_something_cool
end
end