How to use a dynamically chosen class in a module - ruby-on-rails

I'm fairly sure that's a useless title... sorry.
I want to be able to pass in a Class to a method, and then use that class. Here's an easy, working, example:
def my_method(klass)
klass.new
end
Using that:
>> my_method(Product)
=> #<Product id:nil, created_at: nil, updated_at: nil, price: nil>
>> my_method(Order)
=> #<Order id:nil, created_at: nil, updated_at: nil, total_value: nil>
What doesn't work is trying to use the klass variable on a module:
>> ShopifyAPI::klass.first
=> NoMethodError: undefined method `klass' for ShopifyAPI:Module
Am I attempting an impossible task? Can anyone shed some light on this?
Cheers

First off, I don't think this is impossible.
Surely, there is no klass method defined for modules <- this is true because ShopifyAPI.methods.include? "klass" # => false
However, classes are constants in modules. And modules have a constants method that you may use to retrieve classes. The problem with this is method is that is also retrieves constants in the modules that are not classes.
I came up with this workaround for your problem
# get all the classes in the module
klasses = ShopifyAPI.constants.select do |klass|
ShopifyAPI.const_get(klass).class == Class
end
# get the first class in that list
klasses.first

you could also use module_eval:
ShopifyAPI.module_eval {klass}.first
Hope I got your question right :)
irb(main):001:0> module ShopifyAPI
irb(main):002:1> class Something
irb(main):003:2> end
irb(main):004:1> end
=> nil
irb(main):005:0> klass = ShopifyAPI::Something
=> ShopifyAPI::Something
irb(main):006:0> ShopifyAPI::klass
NoMethodError: undefined method `klass' for ShopifyAPI:Module
from (irb):6
from C:/Ruby192/bin/irb:12:in `<main>
irb(main):007:0> ShopifyAPI.module_eval {klass}
=> ShopifyAPI::Something
irb(main):008:0>

Related

Rails how to improve if record exists?

I have this model:
class Device < ActiveRecord::Base
has_many :events
def last_event
events.last
end
end
As you can see, I have a method to get the last event for the device. Now, elsewhere in the Device model I have this method:
def place
self.last_event.place
end
Now, if I don't have any records in Events for this Device I get an error "undefined method `place' for nil:NilClass".
And so I added:
def place
self.last_event.place if self.last_event.present?
end
And this pattern repeated itself throughout the app, I had to add "if self.last_event.present?" so it won't crash in other places too.
I am sure there must be a better way to handle this kind of thing without the need to check if last_event is present everywhere?
Any suggestions?
Thanks,
The try method (an addition of ActiveSupport) allows exactly that. If called on a nil object, it will just return nil too. Thus, both of the following lines are equivalent:
self.last_event.try(:place)
# equivalent to
self.last_event.place if self.last_event
Another option would be to have the method return a blank object which would respond to calls:
class Device < ActiveRecord::Base
has_many :events
def last_event
events.last || Event.new
end
def place
self.last_event.place
end
end
2.0.0p247 :001 > d = Device.new
=> #<Device id: nil, name: nil, created_at: nil, updated_at: nil>
2.0.0p247 :002 > d.place
=> nil
2.0.0p247 :003 > d.last_event
=> #<Event id: nil, device_id: nil, created_at: nil, updated_at: nil, place: nil>
The idea is that if a method always returns an object of the expected type, you never have to worry about subsequent calls encountering a nil object. Of course, this could have other implications - such as the need to determine if you have a valid object or a new one, but this can be checked later with:
2.0.0p247 :005 > d.last_event.new_record?
=> true
In that case you can use delegates
delegate :last, to: events, allow_nil: true, prefix: :event
delegate :place, to: event_last, allow_nil: true

Rails NoMethodError on .save

I ran the code #transaction = Transaction.new Then I gave it some values:
<Transaction id: nil, debit_uri: "d8hmFJ89CIQUZMBoiPMnvWkQJW/bank_...", credit_uri: "d8hmciqLOg9bCIQUZMBoiPMnvWkQJW/cards...", seller_id: 2, buyer_id: 6, product_id: 31, price: #<BigDecimal:b4a6115c,'0.45E2',9(36)>, ship_price: #<BigDecimal:b4a61094,'0.123E3',9(36)>, ship_method: "fedex", created_at: nil, updated_at: nil>
but when I do #transaction.save! bang(!) or not I get the error:
NoMethodError: undefined method `clear' for nil:NilClass
from /home/alain/.rvm/gems/ruby-1.9.3-head/gems/activemodel-3.2.13/lib/active_model/validations.rb:194:in `valid?'
so I don't know where to look for the error being how my model has little to nothing and there is no method called clear.
class Transaction < ActiveRecord::Base
attr_accessible :buyer_id, :credit_uri, :debit_uri, :price, :product_id, :seller_id, :ship_method, :ship_price
require 'balanced'
attr_reader :errors
end
Regarding to Rails codebase, the error comes from:
attr_reader :errors
Look here. Try to remove it from your model.
Why?
Since you override the errors attributes and did not set it when creating your transaction instance, Rails is trying to do:
nil.clear

virtual field in a model part of the model but not in the DB

I have a standard model with a few fields that are saved to a DB, and I need 1 field that doesn't have to be saved.
I tried attr_accessor but that doesn't cover it. Using Attr_accessor I can set and get the field, but it is not part of the model. If I add the models to an array and then see what is in the virtual field is not part of it. I also tried to add the field :headerfield to attr_accessible but that didn't change anything.
How can I get a field that is part of the model but not saved to the database?
The model
class Mapping < ActiveRecord::Base
attr_accessible :internalfield, :sourcefield
attr_accessor :headerfield
end
console output:
1.9.3-p194 :001 > m = Mapping.new
=> #<Mapping id: nil, internalfield: nil, sourcefield: nil, created_at: nil, updated_at: nil, data_set_id: nil>
1.9.3-p194 :002 > m.headerfield = "asef"
=> "asef"
1.9.3-p194 :003 > m
=> #<Mapping id: nil, internalfield: nil, sourcefield: nil, created_at: nil, updated_at: nil, data_set_id: nil>
Because ActiveRecord::Base has custom implementations for the standard serializiation methods (including to_s and as_json), you will never see your model attributes that do not have backing database columns unless you intervene in some way.
You can render it to JSON using the following:
render json: my_object, methods: [:virtual_attr1, :virtual_attr2]
Or you can use the as_json serializer directly:
my_object.as_json(methods: [:virtual_attr1, :virtual_attr2])
The return you see in the console is nothing else but the value of to_s. For this case, code should be better than natural language, take a look in the following code and see if you understand
class A
end
=> nil
A.new
=> #<A:0xb73d1528>
A.new.to_s
=> "#<A:0xb73d1528>"
class A
def to_s
"foobar"
end
end
=> nil
A.new
=> ble
A.new.to_s
=> "ble"
You can see this output because ActiveRecord::Base defines a method to_s that take into account only the attributes that are defined in the database, not the attr_accessor methods, maybe using the attributes call.

rails Model.create(:attr=>"value") returns model with uninitialized fields

This is really stumping me. The process works fine if I go about it with #new and then #save, but #create returns a model instance with all the fields set to nil.
e.g:
Unexpected behavior:
ruby-1.9.2-p0 > EmailDefault.create(:description=>"hi")
=> #<EmailDefault id: nil, description: nil, created_at: nil, updated_at: nil>
Expected behaviour:
ruby-1.9.2-p0 > e = EmailDefault.new
=> #<EmailDefault id: nil, description: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p0 > e.description = "hi"
=> "hi"
ruby-1.9.2-p0 > e.save
=> true
ruby-1.9.2-p0 > EmailDefault.last
=> #<EmailDefault id: 4, description: "hi", created_at: "2011-02-27 22:25:33", updated_at: "2011-02-27 22:25:33">
What am I doing wrong?
--update--
Turns out I was mis-using attr_accessor. I wanted to add some non-database attributes, so I did it with:
attr_accessible :example_to, :cc_comments
which is wrong, and caused the situation #Heikki mentioned. What I need to do is:
attr_accessor :example_to, :cc_comments
You need to white list those properties with attr_accessible to enable mass-assignment.
http://api.rubyonrails.org/classes/ActiveModel/MassAssignmentSecurity/ClassMethods.html#method-i-attr_accessible
--edit
By default all attributes are available for mass-assignment. If attr_accessible is used then mass-assignment will work only for those attributes. Attr_protected works the opposite way ie. those attributes will be protected from mass-assignment. Only one should be used at a time. I prefer the white listing with attr_accessible.

ActiveRecord derived attribute persistence which depends on id value

How do you persist a derived attribute which depends on the value of id in rails? The snippet below seems to work-- Is there a better rails way?
class Model < ActiveRecord::Base
....
def save
super
#derived_attr column exists in DB
self.derived_attr = compute_attr(self.id)
super
end
end
Callbacks are provided so you should never have to override save. The before_save call in the following code is functionally equivalent to all the code in the question.
I've made set_virtual_attr public so that it can be calculated as needed.
class Model < ActiveRecord::Base
...
# this one line is functionally equivalent to the code in the OP.
before_save :set_virtual_attr
attr_reader :virtual_attr
def set_virtual_attr
self.virtual_attr = compute_attr(self.id)
end
private
def compute_attr
...
end
end
I think the more accepted way to do this would be to provide a custom setter for the virtual attribute and then provide an after_create hook to set the value after the record is created.
The following code should do what you want.
class Virt < ActiveRecord::Base
def after_create()
self.virtual_attr = nil # Set it to anything just to invoke the setter
save # Saving will not invoke this callback again as the record exists
# Do NOT try this in after_save or you will get a Stack Overflow
end
def virtual_attr=(value)
write_attribute(:virtual_attr, "ID: #{self.id} #{value}")
end
end
Running this in the console shows the following
v=Virt.new
=> #<Virt id: nil, virtual_attr: nil, created_at: nil, updated_at: nil>
>> v.save
=> true
>> v
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">
>> Virt.last
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">

Resources