Dynamically instantiate Rails nested STI subclass? - ruby-on-rails

Let's say I have a class like:
class Basket < ActiveRecord::Base
has_many :fruits
Where "fruits" is an STI base class having subclasses like "apples", "oranges", etc...
I'd like to be able to have a setter method in Basket like:
def fruits=(params)
unless params.nil?
params.each_pair do |fruit_type, fruit_data|
fruit_type.build(fruit_data)
end
end
end
But, obviously, I get an exception like:
NoMethodError (undefined method `build' for "apples":String)
A workaround I thought of works like this:
def fruits=(params)
unless params.nil?
params.each_pair do |fruit_type, fruit_data|
"#{fruit_type}".create(fruit_data.merge({:basket_id => self.id}))
end
end
end
But that causes the Fruit STI object to be instantiated before the Basket class, and so the basket_id key is never saved in the Fruit subclass (because basket_id doesn't exist yet).
I'm totally stumped. Anyone have any ideas?

Instead of adding a setter method in Basket, add it in Fruit:
class Fruit < ActiveRecord::Base
def type_setter=(type_name)
self[:type]=type_name
end
end
Now you can pass the type in when you build the object through an association:
b = Basket.new
b.fruits.build(:type_setter=>"Apple")
Note that you can't assign :type this way, since it is protected from mass assignment.
EDIT
Oh, you wanted to run different callbacks depending on the subclass? Right.
You could do this:
fruit_type = "apples"
b = Basket.new
new_fruit = b.fruits << fruit_type.titleize.singularize.constantize.new
new_fruit.class # Apple
or define a has_many association for each type:
require_dependency 'fruit' # assuming Apple is defined in app/models/fruit.rb
class Basket
has_many :apples
end
then
fruit_type = "apples"
b = Basket.new
new_fruit = b.send(fruit_type).build
new_fruit.class # Apple

In Ruby terms, "#{x}" is simply equivalent to x.to_s which for String values is exactly the same as the string itself. In other languages, like PHP, you can de-reference a string and treat it as a class, but that's not the case here. What you probably mean is this:
fruit_class = fruit_type.titleize.singularize.constantize
fruit_class.create(...)
The constantize method converts from a string to the equivalent class, but it is case sensitive.
Keep in mind that you're exposing yourself to the possibility someone might create something with fruit_type set to "users" and then go ahead and make an administrator account. What's perhaps more responsible is to do an additional check that what you're making is actually of the right class.
fruit_class = fruit_type.titleize.singularize.constantize
if (fruit_class.superclass == Fruit)
fruit_class.create(...)
else
render(:text => "What you're doing is fruitless.")
end
One thing to watch out for when loading classes this way is that constantize will not auto-load classes like having them spelled out in your application does. In development mode you may be unable to create subclasses that have not been explicitly referenced. You can avoid this by using a mapping table which solves the potential security problem and pre-loading all at once:
fruit_class = Fruit::SUBCLASS_FOR[fruit_type]
You can define this constant like this:
class Fruit < ActiveRecord::Base
SUBCLASS_FOR = {
'apples' => Apple,
'bananas' => Banana,
# ...
'zuchini' => Zuchini
}
end
Using the literal class constant in your model will have the effect of loading them immediately.

Related

what a method 'books=(books)' is doing?

I joined Rails team and maintain the codes.
Some of the objects are controlled by Gem virtus, but I really don't understand like below code is doing.
I understand the result that the attribute 'latest_book' can collect latest book from Books but why it can be done? What 'books=(books)' is doing? and Why 'super books' is here?
class GetBooks
include Virtus.model
include ActiveModel::Model
attribute :books, Array[Book]
attribute :latest_book, Book
def books=(books)
self.latest_book = books.sort_by { |book| book['createdate'] }.last
super books
end
end
Could you help me?
def books=(books) is defining a method called books= which takes a single argument books. Yes, that's confusing. It should probably be def books=(value) or def books=(new_books).
And yes, the = is part of the method name. self.books = value is really syntax sugar for self.books=(value). Again, the method is books=.
super books is super(books). super calls the next inherited or included method of the same name; it's calling books= created by attribute :books, Array[Book]. This is a "method override" which allows you to add to the behavior of an existing method.
When books= is called it updates latest_books and then calls its the original method to set the books attribute.
gb = GetBooks.new
gb.books = [old_book, new_book]
p gb.latest_book # new_book
p gb.books # [old_book, new_book]

Pass table name as parameter to ruby method

I have N number of tables and N number of functions. All functions have same code only table name changes. Can I make a common function to be used by all of these function.
Something like this
def funcN
common_func(tableN)
end
private
def common_func(tablename)
"Some Code"
end
I know there may be multiple ways.. What are the possible ways to do it?
You are very close. Just pass a table name as an argument to funcN:
def funcN(tableN)
common_func(tableN)
end
private
def common_func(tablename)
"Some Code"
end
What are all the possible ways to do it?
Theoretically there are indefinite number of ways to solve some problem, so you will never get an answer to this question.
P.S. Your naming does not follow the conventions. Here is how it would look if it did:
def func_n(table_name)
common_func(table_name)
end
private
def common_func(table_name)
# code omitted
end
If model name is static in funcN then just pass it as the string for example consider post then funcN("Post") or from a rails record funcN(#record.class.to_s)
in private method catch the string param as yours tablename and you can convert it into model by myModel = tablename.constantize
then you can carry on with your line of code on that model myModel
If the function, usually called a method in Ruby, is inside a model it can reference the table_name. You can share the common code using a module and including it in each model which needs it, such as:
class Person < AR::Base
include CommonCode
end
class Fruit < AR::Base
include CommonCode
end
module CommonCode
def do_something
self.table_name
end
end
Person.new.do_something # => 'people'
Fruit.new.do_something # => 'fruits'

How to call a parent class and instead create one of it's children?

I have a model directory structure like this:
/alerts
base_alert.rb
panic_alert.rb
hardware_alert.rb
alert.rb
With the /alerts/x_alert.rb models setup like this:
class base_alert < ActiveRecord::Base
...
end
class panic_alert < base_alert
...
end
class hardware_alert < base_alert
...
end
etc.
Is there any way to call create on alert.rb in the top directory, and, based on a parameter passed, it would create one of the children instead of alert.rb.
I.E. Alert.create({type:"panic_alert"})
And it would create and return one of the panic_alert types of alerts?
By making few changes to the class definitions, like subclassing the Alert from ActiveRecord::Base rather than BaseAlert, you could achieve what you are trying to accomplish.
Following are the updated classes:
# app/models/alert.rb
class Alert < ActiveRecord::Base
end
# app/models/alerts/base_alert.rb
module Alerts
class BaseAlert < ::Alert
end
end
# app/models/alerts/panic_alert.rb
module Alerts
class PanicAlert < BaseAlert
end
end
# app/models/alerts/hardware_alert.rb
module Alerts
class HardwareAlert < BaseAlert
end
end
Following are few ways to create the subclasses from the base class:
#panic_alert = Alert.create!(
type: 'Alerts::PanicAlert', #this has to be string
#other attributes
)
#alert = Alert.new
#alert.type = 'Alerts::PanicAlert' #this has to be string
# assign other attributes, if necessary
#alert.save
#alert = Alert.new
#panic_alert = #alert.becomes(Alerts::PanicAlert) #this has to be class
# assign other attributes, if necessary
#panic_alert.save
You can use the constantize or the safe_constantize methods to do that. What they do is take a string and try to return the class the string refers to. For instance:
"BaseAlert".safe_constantize
=> BaseAlert
or
def method_name(alert_type)
alert_type.safe_constantize.create()
end
The difference between the two is constantize will throw an error if there isn't a match for the string, while safe_constantize will just return nil. Remember, if you pass in a underscored string (say panic_alert) then you would have to camelize it.
What seems like a lifetime ago I created StiFactory for this. That said, I don't find much use for STI these days (hence the lack of maintenance).

is there way to convert object into another class in ruby

let say I have model
User < ActiveRecord::Base
end
and his STI brother
MasqueradeUser < User
end
masquerade_user = MasqueradeUser.find 123
masquerade_user.class
# => MasqueradeUser
Ridiculous as it sounds, is possible to convert this object back to parent class User
masquerade_user.some_magic.class # => User
I know I can override methods like mode_name, is_a?(User) and other so that MasqueradeUser will return values like User
MasqueradeUser < User
def model_name
'User'
end
end
I was just wondering if there is a way to completely downgrade object to parent class instance
You can use becomes function of ActiveRecord - see here.
You can genericize ActiveRecord's becomes method as such:
def becomes(klass)
became = klass.new
became.instance_variable_set("#attributes", #attributes)
became
end
After all it's just a matter of copying variables into another object that supports them (i.e: it can "become" any class, not just the superclass)

Wrapping an object with methods from another class

Let's say I have a model called Article:
class Article < ActiveRecord::Base
end
And then I have a class that is intended to add behavior to an article object (a decorator):
class ArticleDecorator
def format_title
end
end
If I wanted to extend behavior of an article object, I could make ArticleDecorator a module and then call article.extend(ArticleDecorator), but I'd prefer something like this:
article = ArticleDecorator.decorate(Article.top_articles.first) # for single object
or
articles = ArticleDecorator.decorate(Article.all) # for collection of objects
How would I go about implementing this decorate method?
What exactly do you want from decorate method? Should it simply add some new methods to passed objects or it should automatically wrap methods of these objects with corresponding format methods? And why do you want ArticleDecorator to be a class and not just a module?
Updated:
Seems like solution from nathanvda is what you need, but I'd suggest a bit cleaner version:
module ArticleDecorator
def format_title
"#{title} [decorated]"
end
def self.decorate(object_or_objects_to_decorate)
object_or_objects_to_decorate.tap do |objects|
Array(objects).each { |obj| obj.extend ArticleDecorator }
end
end
end
It does the same thing, but:
Avoids checking type of the arguments relying on Kernel#Array method.
Calls Object#extend directly (it's a public method so there's no need in invoking it through send).
Object#extend includes only instance methods so we can put them right in ArticleDecorator without wrapping them with another module.
May I propose a solution which is not using Module mixins and thereby granting you more flexibility. For example, using a solution a bit more like the traditional GoF decorator, you can unwrap your Article (you can't remove a mixin if it is applied once) and it even allows you to exchange the wrapped Article for another one in runtime.
Here is my code:
class ArticleDecorator < BasicObject
def self.[](instance_or_array)
if instance_or_array.respond_to?(:to_a)
instance_or_array.map {|instance| new(instance) }
else
new(instance_or_array)
end
end
attr_accessor :wrapped_article
def initialize(wrapped_article)
#wrapped_article = wrapped_article
end
def format_title
#wrapped_article.title.upcase
end
protected
def method_missing(method, *arguments)
#wrapped_article.method(method).call(*arguments)
end
end
You can now extend a single Article by calling
extended_article = ArticleDecorator[article]
or multiple articles by calling
articles = [article_a, article_b]
extended_articles = ArticleDecorator[articles]
You can regain the original Article by calling
extended_article.wrapped_article
Or you can exchange the wrapped Article inside like this
extended_article = ArticleDecorator[article_a]
extended_article.format_title
# => "FIRST"
extended_article.wrapped_article = article_b
extended_article.format_title
# => "SECOND"
Because the ArticleDecorator extends the BasicObject class, which has almost no methods already defined, even things like #class and #object_id stay the same for the wrapped item:
article.object_id
# => 123
extended_article = ArticleDecorator[article]
extended_article.object_id
# => 123
Notice though that BasicObject exists only in Ruby 1.9 and above.
You'd extend the article class instance, call alias_method, and point it at whatever method you want (although it sounds like a module, not a class, at least right now). The new version gets the return value and processes it like normal.
In your case, sounds like you want to match up things like "format_.*" to their respective property getters.
Which part is tripping you up?
module ArticleDecorator
def format_title
"Title: #{title}"
end
end
article = Article.top_articles.first.extend(ArticleDecorator) # for single object
Should work fine.
articles = Article.all.extend(ArticleDecorator)
May also work depending on ActiveRecord support for extending a set of objects.
You may also consider using ActiveSupport::Concern.

Resources