I have this code...
foo = opt_source.downcase.camelize
"#{foo}".new.start_check
this should call a class and the method start_check, but I am getting an undefined method error
undefined method `start_check' for "Abcd":String (Abcd is the class that foo represents)
Any suggestions on how what I am doing wrong?
You need to convert that string into a constant. Historically this was done with eval but this leads to security issues in your code -- never eval user-supplied strings.
The correct way to do this (in Rails) is via String#constantize:
foo = opt_source.downcase.camelize
foo.constantize.new.start_check
For ruby, use Kernel#const_get:
foo = opt_source.downcase.camelize
Kernel.const_get(foo).new.start_check
Don't forget to check for errors before calling your methods.
Just eval(foo).new.start_check
Create a class name from a plural table name like Rails does for table names to models.
http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-classify
camelize is not fit to convert plural string into active record class if you are calling method from active record
For example:
u = "batteries"
u.camelize #=> "Batteries"
u.classify #=> ""Battery"
Try
opt_source.classify.constantize.new.send(:start_check)
Related
Having a string with the module and name of a class, like:
"Admin::MetaDatasController"
how do I get the actual class?
The following code works if there's no module:
Kernel.const_get("MetaDatasController")
but it breaks with the module:
ruby-1.8.7-p174 > Kernel.const_get("Admin::MetaDatasController")
NameError: wrong constant name Admin::MetaDatasController
from (irb):34:in `const_get'
from (irb):34
ruby-1.8.7-p174 >
If you want something simple that handles just your special case you can write
Object.const_get("Admin").const_get("MetaDatasController")
But if you want something more general, split the string on :: and resolve the names one after the other:
def class_from_string(str)
str.split('::').inject(Object) do |mod, class_name|
mod.const_get(class_name)
end
end
the_class = class_from_string("Admin::MetaDatasController")
On the first iteration Object is asked for the constant Admin and returns the Admin module or class, then on the second iteration that module or class is asked for the constant MetaDatasController, and returns that class. Since there are no more components that class is returned from the method (if there had been more components it would have iterated until it found the last).
ActiveSupport provides a method called constantize, which will do this. If you are on Rails, which I assume you are based on the name of your constant, then you already have ActiveSupport loaded.
require 'active_support/core_ext/string'
class Admin
class MetaDatasController
end
end
"Admin::MetaDatasController".constantize # => Admin::MetaDatasController
To see how the method is implemented, check out https://github.com/rails/rails/blob/85c2141fe3d7edb636a0b5e1d203f05c70db39dc/activesupport/lib/active_support/inflector/methods.rb#L230-L253
In Ruby 2.x, you can just do this:
Object.const_get('Admin::MetaDatasController')
=> Admin::MetaDatasController
i could be way off-base, but wouldn't eval return the class?
eval("Admin::MetaDatasController")
so eval("Admin::MetaDatasController").new would be the same as Admin::MetaDatasController.new
I've used a gem and tried to create a method (trans) in my code.
require 'yandex-translator'
translator = Yandex::Translator.new(api_key)
def trans(text)
a = translator.translate text, to: "ru"
return a
end
puts trans("stack")
When I run the code, I get this error:
'trans': undefined local variable or method `translator' for main:Object (NameError)
Why did I get this error, and how can I solve this?
translator variable in this code is defined on class level, hence it’s a local variable in main context (since the whole code is executed in main context.)
You are trying to call it from the instance context, where it is obviously not defined. The easiest way to overcome it, would be to define #translator as being a class’ instance variable:
#translator = Yandex::Translator.new(api_key)
def trans(text)
#translator.translate text, to: "ru"
end
Because in this way you are looking for a local variable translator and you have not. Some solutions:
make translator global
$translator = Yandex::Translator.new(api_key)
or pass translator to trans method
def trans(translator, text)
translator.translate text, to: "ru"
end
I have a situation in my Rails application where I need to include arbitrary modules depending on the current runtime state. The module provides custom application code that is only needed when certain conditions are true. Basically, I'm pulling the name of a company from the current context and using that as the filename for the module and its definition:
p = self.user.company.subdomain + ".rb"
if File.exists?(Rails.root + "lib/" + p)
include self.class.const_get(self.user.company.subdomain.capitalize.to_sym)
self.custom_add_url
end
My test module looks like this:
module Companyx
def custom_add_url
puts "Calling custom_add_url"
end
end
Now in the console, this actually works fine. I can pull a user and include the module like so:
[1] pry(main)> c = Card.find_by_personal_url("username")
[2] pry(main)> include c.class.const_get(c.user.company.subdomain.capitalize)=> Object
[3] pry(main)> c.custom_add_url
Calling custom_add_url
If I try to run the include line from my model, I get
NoMethodError: undefined method `include' for #<Card:0x007f91f9094fb0>
Can anyone suggest why the include statement would work on the console, but not in my model code?
I'm doing a similar thing. I found this answer useful:
How to convert a string to a constant in Ruby?
Turns out I was looking for the constantize method. This is the line I'm using in my code:
include "ModuleName::#{var.attr}".constantize
Edit:
So ack, I ran into various problems with actually using that line myself. Partially because I was trying to call it inside a method in a class. But since I'm only calling one method in the class (which calls/runs everything else) the final working version I have now is
"ModuleName::#{var.attr}".constantize.new.methodname
Obviously methodname is an instance method, so you could get rid of the new if yours is a class method.
Include is a method on a class.
If you want to call it inside a model, you need to execute the code in the context of its singleton class.
p = self.user.company.subdomain + ".rb"
if File.exists?(Rails.root + "lib/" + p)
myself = self
class_eval do
include self.const_get(myself.user.company.subdomain.capitalize.to_sym)
end
self.custom_add_url
EDIT:
class << self doesn't accept a block; class_eval does, hence it preserves the state of local variables. I've modified my solution to use it.
RuNubie here. I've got a class Login that logs into gmail using the net/IMAP library. What is happening is that I create a new instance of that class, such as:
a = Login.new("username", "gmail.com", "passw")
Then, I'm working on other classes that will do some "stuff" with the mailbox. The problem is that the #imap variable I've defined in Login seems to have disappeared (due to scoping I assume).
This is how #imap is declared in Login class:
#imap = Net::IMAP.new('imap.gmail.com',993,true,nil,false)
So this:
#today = Date.today
#received_today = imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
...returns an error. These are the two errors I've gotten while playing around with this. The first one is when I use imap, the second one is when I try #imap:
NameError: undefined local variable or method `imap' for #<Object:0x10718d2a8>
NoMethodError: undefined method `search' for nil:NilClass
What are the best practices for dealing with a situation like this? Is the only solution to define my methods that do "stuff" in the same class where I'm creating the new instance of Net::IMAP? Is declaring #imap as a global variable $imap a bad practice? So confused, I bet the answer is very simple and obvious too, but I'm just not seeing it. Thanks!
This:
#received_today = imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
won't work because, well, there is no imap in scope at that point and so you get a NameError. When you try it like this:
#received_today = #imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
You get a NoMethodError because instance variables, such as #imap, are automatically created at first use and initialized as nil. Your real #imap is in another object so you can't refer to it as #imap anywhere else.
I think you want a structure more like this:
class User
def imap
if(!#imap)
#imap = Net::IMAP.new('imap.gmail.com', 993, true, nil, false)
# and presumably an #imap.authenticate too...
end
#imap
end
end
class OtherOne
def some_method(user)
#today = Date.today
#received_today = user.imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
end
end
Keep your Net::IMAP localized inside your User and let other objects use it by providing a simple accessor method.
Oh and that global $imap idea, I'll just pretend I didn't see that as globals are almost always a really bad idea.
a shorter way to define the imap variable in the User class, which is pretty much the same as what mu posted:
class User
def imap
#imap ||= Net::IMAP.new...
end
end
Having a string with the module and name of a class, like:
"Admin::MetaDatasController"
how do I get the actual class?
The following code works if there's no module:
Kernel.const_get("MetaDatasController")
but it breaks with the module:
ruby-1.8.7-p174 > Kernel.const_get("Admin::MetaDatasController")
NameError: wrong constant name Admin::MetaDatasController
from (irb):34:in `const_get'
from (irb):34
ruby-1.8.7-p174 >
If you want something simple that handles just your special case you can write
Object.const_get("Admin").const_get("MetaDatasController")
But if you want something more general, split the string on :: and resolve the names one after the other:
def class_from_string(str)
str.split('::').inject(Object) do |mod, class_name|
mod.const_get(class_name)
end
end
the_class = class_from_string("Admin::MetaDatasController")
On the first iteration Object is asked for the constant Admin and returns the Admin module or class, then on the second iteration that module or class is asked for the constant MetaDatasController, and returns that class. Since there are no more components that class is returned from the method (if there had been more components it would have iterated until it found the last).
ActiveSupport provides a method called constantize, which will do this. If you are on Rails, which I assume you are based on the name of your constant, then you already have ActiveSupport loaded.
require 'active_support/core_ext/string'
class Admin
class MetaDatasController
end
end
"Admin::MetaDatasController".constantize # => Admin::MetaDatasController
To see how the method is implemented, check out https://github.com/rails/rails/blob/85c2141fe3d7edb636a0b5e1d203f05c70db39dc/activesupport/lib/active_support/inflector/methods.rb#L230-L253
In Ruby 2.x, you can just do this:
Object.const_get('Admin::MetaDatasController')
=> Admin::MetaDatasController
i could be way off-base, but wouldn't eval return the class?
eval("Admin::MetaDatasController")
so eval("Admin::MetaDatasController").new would be the same as Admin::MetaDatasController.new