ruby convert class name in string to actual class - ruby-on-rails

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`

Related

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.

Make Rails recognise my class inside a string

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

Access object attribute inside module

I have a class with the attribute .weekday saved as an integer value.
I'm trying to create a method in a module that converts that numeric value to the corresponding weekday.
This is how I like to work:
MyClass.weekday
=> 2
MyClass.weekday.my_module_method
=> "Tuesday"
Is it possible to do this conversion with a module method or am I thinking wrong here?
I can access the object from within the module mehtod, by self, but I don't seem to be able to do self.weekday.
What you are trying to do is certainly possible. You are correct when you point to ActiveRecord::Inflector as something that works similarly. This approach modifies the Fixnum class itself to add new methods and although I don't generally recommend ad-hoc patching of core classes, you can see it in action in active_support/core_ext/integer/inflections.rb :
require 'active_support/inflector'
class Integer
# Ordinalize turns a number into an ordinal string used to denote the
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# 1.ordinalize # => "1st"
# 2.ordinalize # => "2nd"
# 1002.ordinalize # => "1002nd"
# 1003.ordinalize # => "1003rd"
# -11.ordinalize # => "-11th"
# -1001.ordinalize # => "-1001st"
#
def ordinalize
ActiveSupport::Inflector.ordinalize(self)
end
end
In your case I might do something like:
module WeekdayInflector
def weekday
Date::DAYNAMES[self]
end
end
class Fixnum
include WeekdayInflector
end
which will at least help others track down the methods you added by looking at the module. Please note that this will affect ALL instances of Fixnum and could lead to conflicts if you include a Gem that tries to do the same thing. It is worth asking whether this tradeoff is worth it or if defining a simple view helper is the better way to go.

Overwriting default accessors with options

I am using Ruby on Rails 4 and I would like to know what could be the pitfalls when I overwrite default accessors. The Rails' Official Documentation says (in the initial lines):
The mapping that binds a given Active Record class to a certain
database table will happen automatically in most common cases, but can
be overwritten for the uncommon ones.
More, in that documentation there is the "Overwriting default accessors" section which makes me think that I can do it without any problem. What do you think about?
In my case I would like to overwrite attribute accessors in order to provide some options, something like this:
# Given my Article model has :title and :content attributes
# Then I would like to overwrite accessors providing options this way:
class Article < ActiveRecord::Base
def title(options = {})
# Some logic...
end
def content(options = {})
# Some logic...
end
end
# So that I can run
#article.title # => "Sample Title"
#article.title(:parse => true) # => "Sample *Title*"
#article.content # => "Sample description very long text"
#article.content(:length => :short) # => "Sample description..."
Maybe this is more Ruby than Rails, but will be the #article.title calling the title(options => {}) method or it will call the Rails attribute accessor that access the related database table column value?
Update (after commenting)
Since it seems that in the above code default accessors are not overwritten, is there a way to provide options for those accessors so to reach what I am looking for? If so, how?
#article.title #=> will call your method
#article.title(:parse => true) #=> will call your method
There is no method overloading in ruby if that is what you are looking for.
Looking closer at the official documentation I see where your code diverges.
You forgot "=" when defining your method.
class Foo < ActiveRecord::Base
def self.bar=(value)
#foo = value
return 'OK'
end
end
Foo.bar = 3 #=> 3
WARNING: Never rely on anything that happens inside an assignment method,
(eg. in conditional statements like in the example above)

Pretty Paths in Rails

I have a category model and I'm routing it using the default scaffolding of resources :categories. I'm wondering if there's a way to change the paths from /category/:id to /category/:name. I added:
match "/categories/:name" => "categories#show"
above the resources line in routes.rb and changed the show action in the controller to do:
#category = Category.find_by_name(params[:name])
it works, but the 'magic paths' such as link_to some_category still use the :id format.
Is there a way to do this? If this is a bad idea (due to some possible way in which rails works internally), is there another way to accomplish this? So that /categories/music, for example, and /categories/3 both work?
Rails has a nifty model instance method called to_param, and it's what the paths use. It defaults to id, but you can override it and produce something like:
class Category < ActiveRecord::Base
def to_param
name
end
end
cat = Category.find_by_name('music')
category_path(cat) # => "/categories/music"
For more info, check the Rails documentation for to_param.
EDIT:
When it comes to category names which aren't ideal for URLs, you have multiple options. One is, as you say, to gsub whitespaces with hyphens and vice versa when finding the record. However, a safer option would be to create another column on the categories table called name_param (or similar). Then, you can use it instead of the name for, well, all path and URL related business. Use the parameterize inflector to create a URL-safe string. Here's how I'd do it:
class Category < ActiveRecord::Base
after_save :create_name_param
def to_param
name_param
end
private
def create_name_param
self.name_param = name.parameterize
end
end
# Hypothetical
cat = Category.create(:name => 'My. Kewl. Category!!!')
category_path(cat) # => "/categories/my-kewl-category"
# Controller
#category = Category.find_by_name_param(param[:id]) # <Category id: 123, name: 'My. Kewl. Category!!!'>
If you don't want to to break existing code that relying on model id you could define your to_param like this:
def to_param
"#{id}-#{name}"
end
so your url will be: http://path/1-some-model and you still can load your model with Model.find(params[:id]) because:
"123-hello-world".to_i
=> 123
Although possibly more than you need, you may also want to look into 'human readable urls' support like friendly_id or one of the others (for instance, if you need unicode support, etc.) that are described here at Ruby Toolbox.

Resources