Make Rails recognise my class inside a string - ruby-on-rails

I have a variable that stores the name of a class
my_class = "Homework"
This class has certain attributes which I would like to access
Homework.find_by
How can I make ruby see this string as an object?
e.g.
my_class.find_by

You can use classify and constantize
my_class.classify.constantize.find_by # something
classify
Create a class name from a plural table
name like Rails does for table names to models. Note that this returns
a string and not a Class (To convert to an actual class follow
classify with constantize).
constantize
Tries to find a constant with the name specified in the argument
string.
'Module'.constantize # => Module
'Test::Unit'.constantize # => Test::Unit
If you are sure about your input, you need only constantize
my_class.constantize.find_by # something

Related

Ruby on Rails: safely get the model class from a model_name

I have a polymorphic route that accepts the name of an ActiveRecord model (e.g. "User", "UserGroup") as a parameter.
How can I safely access the class based on the parameter?
The naive implementation (and likely not safe) would be:
model_class = params[:modelName].constantize
How can this be achieved without causing vulnerabilities?
I would use an explicit allowlist of models that the user is allowed to constantize in this context:
allowed_classes = ["User", "UserGroup"]
class_name = params[:modelName].presence_in(allowed_classes)
if class_name.present?
model_class = class_name.safe_constantize
else
# handle error
end
presence_in returns the string if it is included in the array, nil otherwise.
I'd say that you would need a validation on that param, that checks if its value is in the collection of acceptable models, and after that, do indeed use .constantize.
If you accept all models, and all of them inherit from ApplicationRecord or something, you could generate the collection like this:
ApplicationRecord.subclasses.collect { |type| type.name }.sort
And check if params[:modelName] is in this collection.
Also take note that constantize does throw an error if no constant exists to match the result:
[1] pry(main)> "NotAConstant".constantize
NameError: uninitialized constant NotAConstant

Rails ActiveAttr Gem, manipulation of attributes within Class?

I have a Rails 5 class which includes ActiveAttr::Model, ActiveAttr:MassAssignment and ActiveAttr::AttributeDefaults.
It defines a couple of attributes using the method attribute and has some instance methods. I have some trouble manipulating the defined attributes. My problem is how to set an attribute value within the initializer. Some code:
class CompanyPresenter
include ActiveAttr::Model
include ActiveAttr::MassAssignment
include ActiveAttr::AttributeDefaults
attribute :identifier
# ...
attribute :street_address
attribute :postal_code
attribute :city
attribute :country
# ...
attribute :logo
attribute :schema_org_identifier
attribute :productontology
attribute :website
def initialize(attributes = nil, options = {})
super
fetch_po_field
end
def fetch_po_field
productontology = g_i_f_n('ontology') if identifier
end
def uri
#uri ||= URI.parse(website)
end
# ...
end
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable (g_i_f_n(...) is defined farther down, it works and its return value is correct). The only way I have found to set this variable is to write self.productontology instead. Moreover, the instance variable #uri is not defined as an attribute, instead it is written down only in this place and visible from outside.
Probably I have simply forgotten the basics of Ruby and Rails, I've done this for so long with ActiveRecord and ActiveModel. Can anybody explain why I need to write self.productontology, using #productontology doesn't work, and why my predecessor who wrote the original code mixed the # notation in #uri with the attribute-declaration style? I suppose he must have had some reason to do it like this.
I am also happy with any pointers to documentation. I haven't been able to find docs for ActiveAttr showing manipulation of instance variables in methods of an ActiveAttr class.
Thank you :-)
To start you most likely don't need the ActiveAttr gem as it really just replicates APIs that are already available in Rails 5.
See https://api.rubyonrails.org/classes/ActiveModel.html.
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable.
This is really just a Ruby thing and has nothing to do with the Rails Attributes API or the ActiveAttr gem.
When using assignment you must explicitly set the recipient unless you want to set a local variable. This line:
self.productontology = g_i_f_n('ontology') if identifier
Is actually calling the setter method productontology= on self using the rval as the argument.
Can anybody explain why I need to write self.productontology, using
#productontology doesn't work
Consider this plain old ruby example:
class Thing
def initialize(**attrs)
#storage = attrs
end
def foo
#storage[:foo]
end
def foo=(value)
#storage[:foo] = value
end
end
irb(main):020:0> Thing.new(foo: "bar").foo
=> "bar"
irb(main):021:0> Thing.new(foo: "bar").instance_variable_get("#foo")
=> nil
This looks quite a bit different then the standard accessors you create with attr_accessor. Instead of storing the "attributes" in one instance variable per attribute we use a hash as the internal storage and create accessors to expose the stored values.
The Rails attributes API does the exact same thing except its not just a simple hash and the accessors are defined with metaprogramming. Why? Because Ruby does not let you track changes to simple instance variables. If you set #foo = "bar" there is no way the model can track the changes to the attribute or do stuff like type casting.
When you use attribute :identifier you're writing both the setter and getter instance methods as well as some metadata about the attribute like its "type", defaults etc. which are stored in the class.

Simple CSV import function in Rails that takes the 'table' name as a variable?

I am creating an application that has many different types of objects and I want to enable the user to upload to any of them. There are lots of guides online to creating an import function within 1 specific model, but I do not want to duplicate the code within each model. Is there a neater way?
For example, Product.create! row.to_hash, how can I use a variable instead of hardcoding 'Product'?
constantize will turn a class name into an actual class object.
# params = { table: 'products' }
klass = params[:table].classify.constantize # => Product
klass.create! row.to_hash # or whatever

How do I name a Rails ruby file whose class name has numbers?

Can a Rails class name contain numbers? For example:
class Test123
end
Is this a valid class? I get an uninitialized constant Test123 error when I try to load the class.
I think Artem Kalinchuk's last comment deserves to be the answer of this misworded question.
A Ruby class name can contain numbers.
A Rails class has to be defined in a correctly named file. If I define a class called NewYear2012Controller:
Correct file name: new_year2012_controller.rb
Incorrect file name: new_year_2012_controller.rb (note the extra underscore)
Because this is how Rails inflector and auto-loading works.
Yes, Ruby class names may contain numbers. However, as with all identifiers in Ruby, they may not begin with numbers.
Reference:
Identifiers
Examples:
foobar ruby_is_simple
Ruby identifiers are consist of alphabets,
decimal digits, and the underscore character, and begin with a
alphabets(including underscore). There are no restrictions on the
lengths of Ruby identifiers.
Try to do this:
rename your model and model.rb file
add table_name magic
as here:
class TwoProduct < ActiveRecord::Base
self.table_name = '2_products'
end
I don't know about this...
See the following
class Ab123
def initialize(y)
#z = y
end
end
class AbCde
def initialize(y)
#z = y
end
end
and the following instantiations:
Ab123.new x
or
AbCde.new x
Only the latter AbCde.new x instantiates properly.

ruby convert class name in string to actual class

How do I call a class from a string containing that class name inside of it? (I guess I could do case/when but that seems ugly.)
The reason I ask is because I'm using the acts_as_commentable plugin, among others, and these store the commentable_type as a column. I want to be able to call whatever particular commentable class to do a find(commentable_id) on it.
Thanks.
I think what you want is constantize
That's an RoR construct. I don't know if there's one for ruby core
"Object".constantize # => Object
It depends on the string...
If it already has the proper shape (casing, pluralization, etc), and would otherwise map directly to an object, then:
Rails:
'User'.constantize # => User
Ruby:
Module.const_get 'User' # => User
But otherwise (note the difference in casing):
'user'.constantize # => NameError: wrong constant name user
Module.const_get 'user' # => NameError: wrong constant name user
Therefore, you must ask... is the source string singular or plural (does it reference a table or not?), is it multi-word and AlreadyCamelCased or is_it_underscored?
With Rails you have these tools at your disposal:
Use camelize to convert strings to UpperCamelCase strings, even handling underscores and forward slashes:
'object'.constantize # => NameError: wrong constant name object
'object'.camelize # => "Object"
'object'.camelize.constantize # => Object
'active_model/errors'.camelize # => "ActiveModel::Errors"
'active_model/errors'.camelize.constantize # => ActiveModel::Errors
Use classify to convert a string, which may even be plural (i.e. perhaps it's a table reference), to create a class name (still a string), then call constantize to try to find and return the class name constant (note that in Ruby class names are constants):
'users'.classify => "User" # a string
'users'.classify.constantize # => User
'user'.classify => "User" # a string
'user'.classify.constantize # => User
'ham_and_eggs'.classify # => "HamAndEgg"
In POR (Plain Old Ruby), you have capitalize, but it only works for the first word:
Module.const_get 'user'.capitalize => User
...otherwise you must use fundamental tools like strip, split, map, join, etc. to achieve the appropriate manipulation:
class HamAndEgg end # => nil
Module.const_get ' ham and eggs '.strip.gsub(/s$/,'').split(' ').map{|w| w.capitalize}.join # => HamAndEgg
I know this is an old question but I just want to leave this note, it may be helpful for others.
In plain Ruby, Module.const_get can find nested constants. For instance, having the following structure:
module MyModule
module MySubmodule
class MyModel
end
end
end
You can use it as follows:
Module.const_get("MyModule::MySubmodule::MyModel")
MyModule.const_get("MySubmodule")
MyModule::MySubmodule.const_get("MyModel")
When ActiveSupport is available (e.g. in Rails): String#constantize or String#safe_constantize, that is "ClassName".constantize.
In pure Ruby: Module#const_get, typically Object.const_get("ClassName").
In recent rubies, both work with constants nested in modules, like in Object.const_get("Outer::Inner").
If you want to convert string to actuall class name to access model or any other class
str = "group class"
> str.camelize.constantize 'or'
> str.classify.constantize 'or'
> str.titleize.constantize
Example :
def call_me(str)
str.titleize.gsub(" ","").constantize.all
end
Call method : call_me("group class")
Result:
GroupClass Load (0.7ms) SELECT `group_classes`.* FROM `group_classes`

Resources