easy way to check up series of consistency in rails model object - ruby-on-rails

In my ruby on rails project, I have a model of different parts: A, B, C. A has different fields, depending on that which entity of B and C may fit in. When user put A,B,C in cart I have to tell them if A,B,C these parts fits in or not?
class A < ActiveRecord::Base
def supports_x(B)
...
return true
end
def supports_y(C)
...
return true
end
....10/15 methods like this
def report(cart)
report=[]
if support_x(cart.a)
report<<"a does not support x opporation"
end
.... like this 10/15 hand written if else operation.
end
end
Personally I can do that, I can write 10/15 similar looking methods and if else blocks, but my instinct is telling me I am doing it wrong, I am violating DRY principle.
Is there any way I can avoid this in ruby/rails?

You can use the method_missing method like this:
class A < ActiveRecord::Base
def method_missing(m, *args, &block)
if m.to_s.start_with? 'support_'
# Check the full method name and do whatever you want with args
else
super
end
end
def report(cart)
report=[]
parts = ['x', 'y', 'z']
parts.each do |part|
if self.send("support_#{part}", cart.a)
report << "a does not support #{part} operation"
end
end
end
end
Note that I haven't tested this, and it's more like a pseudocode to give you an idea.

Related

Dynamically defining instance method within an instance method

I have a several classes, each of which define various statistics.
class MonthlyStat
attr_accessor :cost, :size_in_meters
end
class DailyStat
attr_accessor :cost, :weight
end
I want to create a decorator/presenter for a collection of these objects, that lets me easily access aggregate information about each collection, for example:
class YearDecorator
attr_accessor :objs
def self.[]= *objs
new objs
end
def initialize objs
self.objs = objs
define_helpers
end
def define_helpers
if o=objs.first # assume all objects are the same
o.instance_methods.each do |method_name|
# sums :cost, :size_in_meters, :weight etc
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
end
end
end
YearDecorator[mstat1, mstat2].yearly_cost_sum
Unfortunately define method isn't available from within an instance method.
Replacing this with:
class << self
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
...also fails because the variables method_name and objs which are defined in the instance are no longer available. Is there an idomatic was to accomplish this in ruby?
(EDITED: I get what you're trying to do now.)
Well, I tried the same approaches that you probably did, but ended up having to use eval
class Foo
METHOD_NAMES = [:foo]
def def_foo
METHOD_NAMES.each { |method_name|
eval <<-EOF
def self.#{method_name}
\"#{method_name}\".capitalize
end
EOF
}
end
end
foo=Foo.new
foo.def_foo
p foo.foo # => "Foo"
f2 = Foo.new
p f2.foo # => "undefined method 'foo'..."
I myself will admit it's not the most elegant solution (may not even be the most idiomatic) but I've run into similar situations in the past where the most blunt approach that worked was eval.
I'm curious what you're getting for o.instance_methods. This is a class-level method and isn't generally available on instances of objects, which from what I can tell, is what you're dealing with here.
Anyway, you probably are looking for method_missing, which will define the method dynamically the first time you call it, and will let you send :define_method to the object's class. You don't need to redefine the same instance methods every time you instantiate a new object, so method_missing will allow you to alter the class at runtime only if the called method hasn't already been defined.
Since you're expecting the name of a method from your other classes surrounded by some pattern (i.e., yearly_base_sum would correspond to a base method), I'd recommend writing a method that returns a matching pattern if it finds one. Note: this would NOT involve making a list of methods on the other class - you should still rely on the built-in NoMethodError for cases when one of your objects doesn't know how to respond to message you send it. This keeps your API a bit more flexible, and would be useful in cases where your stats classes might also be modified at runtime.
def method_missing(name, *args, &block)
method_name = matching_method_name(name)
if method_name
self.class.send :define_method, name do |*args|
objs.inject(0) {|obj, sum| sum + obj.send(method_name)}
end
send name, *args, &block
else
super(name, *args, &block)
end
end
def matching_method_name(name)
# ... this part's up to you
end

What's a really good pattern to extract logic from models and have the results persisted to the db?

I've been experimenting with different ways of structuring my app, and particularly with ActiveRecord-based models, looking into the notion of having a separate class whose results are saved as a single field. Now composition might have worked, but there's been a lot of discussion on whether its staying or not, so I was wondering what alternatives you might have.
Example:
class Gadget < ActiveRecord::Base
attr_accessor: lights
end
Now, I would like the lights property to be 'managed' by a completely separate class:
class Lights
def doSomething
end
def doSomethingElse
end
end
What's a good way of, say, proxying or delegating this logic out of the ActiveRecord model and into the Lights class in order to transparently store the results?
e.g. using an instance of the Lights class as the lights property - but that won't work, since there's no association, right?
e.g. use method_missing to push all requests out to the lights instance - but that won't work either, because it won't be persisted to the database.
Maybe it's not possible, but I'm interested in ways of persisting the results of logical operations. All suggestions welcome.
What about something like:
class Gadget < ActiveRecord::Base
def lights
#lights ||= Lights.new(self)
end
end
class Lights
attr_reader :root
delegate :save, :save!, to: :root
def initialize(root)
#root = root
end
def doSomething
end
def doSomethingElse
end
def method_missing(method_name, *args, &block)
#delegate all setters to root
if method_name =~ /.*=$/
root.send(method_name, *args, &block)
else
super
end
end
end
Thus you can do:
gadget = Gadget.new
#say gagdet has a name column
gadget.lights.name = 'foo'
gadget.lights.save
gadget.name #=> 'foo'
Still unsure why you need it but it should work

Refactoring methods which depend on __method__

So I'm taking my first step into using Presenters for my rails app and I'm just looking at a refactoring of some of my code. I have several fields which display phone numbers (i.e. phone, cell and fax) nicely formatted or show "none given". Obviously I originally had this in the view but moved the logic into my presenter. Once there I noticed it was all the same so refactored it into a private method which uses the method name and the send function:
class CustomerPresenter < BasePresenter
presents :customer
def phone
format_number(__method__)
end
def cell
format_number(__method__)
end
def fax
format_number(__method__)
end
private
def format_number(method)
hande_none customer.send(method) do
h.number_to_phone(customer.send(method), :area_code => true)
end
end
end
and yet this code still doesn't seem DRY. Because format_number uses the method name it seems that I have to define three seperate methods. I was curious if there was something more I could do here.
p.s. hande_none simply returns the block if there is something there or returns "none given"
I generally avoid to mix actual getter/method/attributes names with the methods used to format them.
That's why I'd use *_formatted suffix: with a general suffix, you can have a simple method_missing which would lead you to something like:
class CustomerPresenter < BasePresenter
presents :customer
private
def format_number(method)
hande_none customer.send(method) do
h.number_to_phone(customer.send(method), :area_code => true)
end
end
def method_missing(method_name, *args, &block)
case method_name
when /(.*)_formatted$/
#here you could create the method on the fly to avoid method missing next time
return format_number( $1 )
end
super(method_name, *args, &block)
end
end
Basically, I've this in my BasePresenter for *_currency_format

Overriding ActiveRecord collection (association) methods

I'm new to Ruby and I'm trying to create a model where collections are assembled if they do not exist.
I already overloaded individual attributes like so:
class My_class < ActiveRecord::Base
def an_attribute
tmp = super
if tmp
tmp
else
#calculate this and some similar, associated attributes, for example:
self.an_attribute = "default" #{This does work as it is calling an_attribute=() }
self.an_attribute
end
end
end
test = My_class.new
p test.an_attribute # => "default"
This works great and basically gives me ||= functionality.
So, without thinking, I went and wrote myself a pretty big function to do the same with a collection (i.e. if there are no objects in the collection, then go and work out what they should be)
class My_class < ActiveRecord::Base
def things
thngs = super
if thngs.empty? #we've got to assemble the this collection!
self.other_things.each do |other_thing| #loop through the other things
#analysis of the other_things and creation and addition of things
self.things << a_thing_i_want
end
else #just return them
thngs
end
end
end
test = My_class.new
test.other_things << other_thing1
test.other_things << other_thing2
p test.things # => METHOD ERROR ON THE 'super' ARGH FAIL :(
This fails unfortunately. Any ideas for solutions? I don't think extensions are the right solution here
Attempted so far:
super --> 'method_missing': super: no superclass method
self[:things] --> test.things.each generates a nilClass error
Potential solution:
Would be rename the table column and has_many association from things to priv_things. This would allow me to simply create the things method and use self.priv_things instead of super

How do I modify Rails ActiveRecord query results before returning?

I have a table with data that needs to be updated at run-time by additional data from an external service. What I'd like to do is something like this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data.each do |object|
puts object.some_attribute_from_external_data_source
end
Even if I can't use this exact syntax, I would like the end result to respect any scopes I may use. I've tried this:
def self.enhance_with_external_data
external_data = get_external_data
Enumerator.new do |yielder|
# mimick some stuff I saw in ActiveRecord and don't quite understand:
relation.to_a.each do |obj|
update_obj_with_external_data(obj)
yielder.yield(obj)
end
end
end
This mostly works, except it doesn't respect any previous scopes that were applied, so if I do this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data
It gives back ALL MyModels, not just the ones scoped by some_custom_scope and some_other_scope.
Hopefully what I'm trying to do makes sense. Anyone know how to do it, or am I trying to put a square peg in a round hole?
I figured out a way to do this. Kind of ugly, but seems to work:
def self.merge_with_extra_info
the_scope = scoped
class << the_scope
alias :base_to_a :to_a
def to_a
MyModel.enhance(base_to_a)
end
end
the_scope
end
def self.enhance(items)
items.each do |item|
item = add_extra_item_info(item)
end
items
end
What this does is add a class method to my model - which for reasons unknown to me seems to also make it available to ActiveRecord::Relation instances. It overrides, just for the current scope object, the to_a method that gets called to get the records. That lets me add extra info to each record before returning. So now I get all the chainability and everything like:
MyModel.where(:some_attribute => some_value).merge_with_extra_info.limit(10).all
I'd have liked to be able to get at it as it enumerates versus after it's put into an array like here, but couldn't figure out how to get that deep into AR/Arel.
I achieved something similar to this by extending the relation:
class EnhancedModel < DelegateClass(Model)
def initialize(model, extra_data)
super(model)
#extra_data = extra_data
end
def use_extra_data
#extra_data.whatever
end
end
module EnhanceResults
def to_a
extra_data = get_data_from_external_source(...)
super.to_a.map do |model_obj|
EnhancedModel.new(model_obj, extra_data)
end
end
end
models = Model.where('condition')
models.extend(EnhanceResults)
models.each do |enhanced_model|
enhanced_model.use_extra_data
end

Resources