Ruby call child method from parent class - ruby-on-rails

I'm writing a program in rails where one class has the same behavior as another class. The only difference is that there is a class variable, #secret_num, that is calculated differently between the two classes. I would like to call a particular super class method, but use the class variable from the child class. What is tricky is that the class variable is not a constant so I am setting it within its own method. Is there any way to do what I'm attempting to do below?
Thanks
Class Foo
def secret
return [1,2,3].sample
end
def b
#secret_num = secret
... # lots of lines of code that use #secret_num
end
end
Class Bar < Foo
def secret
return [4, 5, 6].sample
end
def b
super # use #secret_num from class Bar.
end
end
This doesn't work because the call to super also called the parent class's secret method, i.e. Foo#secret, but I need to use the secret number from the child class, i.e. Bar#secret.

class Foo
def secret
[1,2,3].sample
end
def b(secret_num = secret)
<lots of lines of code that use secret_num>
end
end
class Bar < Foo
def secret
[4, 5, 6].sample
end
end
Note, you don't need to pass secret as an argument to b. As long as you don't redefine b in the subclass, inheritance will take care of calling the correct implementation of secret.
My preference is to have it as an argument so I can pass in various values in testing.

Related

Ruby variables visibility inside modules

TL;DR;
I need to make some ##vars of a static method (extends) in one module visible to a instance method in another module(includes).
How to accomplish that once only setting ##var=value was not enough to make it visible?
Maybe you can just read my capitalized comment bellow and jump to question 4.
Hi, I would like to add an method to my models to index some data in a mysql table with some full text search fields.
In order to accomplish that, I created the following module:
module ElasticFakeIndexing
module IndexingTarget
#instance method to be called on model to get data to save
def build_index_data
{
entity_id: self.id,
entity_type: self.class.name,
#UNABLE TO ACCESS IF SET ONLY WITH ##var=value. Why?
#AND ALMOST SURE THAT USING class_variable_set IS THE CAUSE OF CONFIGURATION OF ONE MODULE MESSING UP WITH ANOTHER'S
title: ##title_fields.collect{|prop| self.send(prop.to_sym)}.join(" || "),
description: ##description_fields.collect{|prop| self.send(prop.to_sym)}.join(" || "),
}
end
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
#class method to declare/call at a given model
def elastic_fake(options = {})
#Make sure we always get an array so we can use 'join'
title_arg = Array(options[:title])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##title_fields, title_arg)
description_arg = Array(options[:description])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##description_fields, description_arg)
extra_arg = Array(options[:extra])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##extra_args, extra_arg)
end
end
end
end
And I use it this way at my models:
class SomeModel < ApplicationRecord
#includes the module
include ElasticFakeIndexing::IndexingTarget
...
# 'static' method call to configure to all classes of this model
elastic_fake(title: "prop_a", description: ["prop_b", "prop_c", "prop_d"])
end
And at some point of my code something like this will be called:
index_data = some_model_instance.build_index_data
save_on_mysql_text_search_fields(index_data)
But I got some problems. And have some questions:
when I use/include my module in a second model, looks like the configuration of one model is being visible to the other. And I got 'invalid fields' like errors. I guess it happens because of this, for example:
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##title_fields, title_arg)
But I got to this this because only set ##title_fields wasn't enough to make title_fields visible at build_index_data instance method. Why?
Why using only #title_fields isn't enough too to make it visible at build_index_data?
How to design it in a way that the set of fields are set in a 'static' variable for each model, and visible inside the instance method build_index_data? Or as a possible solution, the fields could live in a instance variable and be visible. But I think it should live in a 'static' variable because the fields will not change from one instance of the model to another...
Any thoughts? What am I missing about the variables scopes/visibility?
Thank you
Read the following articles on Ruby variables:
Ruby Variable Scope
Understanding Scope in Ruby
quick reminder: ##title_fields, class variable, must be initialized at creation time, while #title_fields, instance variable, hasn't such requirement.
Instead of relying on class variables I recommend using class side instance variables. Class variables will easily be overwritten between individual models including the module. Class side instance variables however are save.
Using some of the syntactic sugar (namely concern and class_attribute) rails offers you could write something like
module ElasticFakeIndexing
extend ActiveSupport::Concern
included do
class_attribute :title_fields,
:description_fields,
:extra_args
end
class_methods do
def elastic_fake(options = {})
...
self.title_fields = Array(options[:title])
...
end
end
def build_index_data
...
title: self.class.title_fields ...
...
end
end

Pass a symbol to a method and call the corresponding method

In a Rails controller you can pass a symbol to the layout method that corresponds to a method in you controller that will return the layout name like this:
layout :my_method
def my_method
'layout_1'
end
I want to have a similar functionality to likewise pass a symbol to my classes method and that class should call the corresponding function and use its return value, like this
myClass.foo :my_method
def my_method
'layout_1'
end
I've read posts[1] that tell me I need to pass
myClass.foo(method(:my_method))
which I find ugly and inconvenient. How is rails here different allowing to pass just the symbol without any wrapper? Can this be achieved like Rails does it?
[1] How to implement a "callback" in Ruby?
If you want to only pass a :symbol into the method, then you have to make assumptions about which method named :symbol is the one you want called for you. Probably it's either defined in the class of the caller, or some outer scope. Using the binding_of_caller gem, we can snag that information easily and evaluate the code in that context.
This surely has security implications, but those issues are up to you! :)
require 'binding_of_caller'
class Test
def foo(sym)
binding.of_caller(1).eval("method(:#{sym})").call
end
end
class Other
def blork
t = Test.new
p t.foo(:bar)
p t.foo(:quxx)
end
def bar
'baz'
end
end
def quxx
'quxx'
end
o = Other.new
o.blork
> "baz"
> "quxx"
I still don't understand, what is author asking about. He's saying about "callbacks", but only wrote how he wants to pass parameter to some method. What that method(foo) should do - i have no idea.
So I tried to predict it's implementation. On class initialising it gets the name of method and create private method, that should be called somewhere under the hood. It possible not to create new method, but store method name in class variable and then call it somewhere.
module Foo
extend ActiveSupport::Concern
module ClassMethods
def foo(method_name)
define_method :_foo do
send method_name
end
end
end
end
class BaseClass
include Foo
end
class MyClass < BaseClass
foo :my_method
private
def my_method
"Hello world"
end
end
MyClass.new.send(:_foo)
#=> "Hello world"
And really, everything is much clearer when you're not just wondering how it works in rails, but viewing the source code: layout.rb

Use attribute of a derived class in parent

I'm trying to use define_method to create additional methods for classes inheriting from a superclass:
class Child < Parent
ADDITIONAL_METHODS += ['xyz', 'qwe']
end
class Parent
ADDITIONAL_METHODS = ['common']
ADDITIONAL_METHODS.each do |key|
define_method key do
...
end
end
end
This doesn't work because ADDITIONAL_METHODS is always taken from the Parent class and the only method created is common. Is there a way to access the attribute from the derived class?
The example code would not work, because you use Parent as ancestor of Child before declaring Parent.
This would produce this error :
uninitialized constant Parent (NameError)
If it actually works for you, it means that Parent has indeed be declared before Child. In that case, the #each loop on ADDITIONAL_METHODS is performed before Child even exists, since instructions you give in a class outside method definition are executed right away :
class Foo
def initialize
puts "second"
end
puts "first"
end
Foo.new
puts "third"
Outputs :
first
second
third
Solution
You may want to implement a class method and call it right away, to perform that.
class Parent
private
def self.add_my_methods( *methods )
( methods.empty? ? [ 'common' ] : methods ).each do |key|
define_method key do
p key
end
end
end
add_my_methods # will implement "common"
end
class Child < Parent
add_my_methods 'xyz', 'qwe'
end
c = Child.new
c.common # outputs "common"
c.xyz # outputs "xyz"
c.qwe # outputs #qwe"
This is an usual pattern for metaprogramming on descendants, like you probably already encountered it with methods like #has_many, #before_filter, etc.

How can I call methods within controller (RoR)?

I am very new to RoR and I have played around the source code. But I have a problem that I already built a 'def A' for creating first CSV file, and 'def B' for creating second CSV file. Each 'def' has its own button, but I have the third button to create all CSVs (to produce output from first and second CSV files.)
What is the possible way to do it?
def first_csv
...
end
def second_csv
..
end
def all_csv
<< how to call get first and second csv >>
end
Thanks in advance,
It should be as simple as you imagine:
def all_csv
first_csv
second_csv
end
Muntasim's answer is correct, but I have to add some additional information.
Ruby provides two types of methods..class methods and instance methods.
class MyClass < AvtiveRecord::Base
# class method
def self.foo
# do something
# within this scope the keyword "self" belongs to the class
end
# another class method which calls internally the first one
def self.bar
something = foo # self.foo could also be written
# do something with something
# within this scope the keyword "self" belongs to the class
end
# instance method
def foo
# do something
# if you use the keyword "self" within an instance method, it belongs to the instance
end
# another instance method which calls class and the first instance method
def bar
mystuff = Myclass.bar # if you want to call the class method you cannot use "self", you must directly call the class
mystuff2 = foo # self.foo is the same, because of the instance scope
return [mystuff, mystuff2]
end
end
You can call the last instance method like following
instance = MyClass.first
instance.bar

rails different ways of defining methods

I've been studing Rails for a not such a long time up to now .... so if there are feel free to correct me
I see that there are two ways of defining methods in rails
def method_name(param)
def self.method_name(param)
The difference (as i understand) is that 1 is mainly used in controllers while 2 is used in models... but occasionaly i bump into methods in models that're defined like 1.
Could you explain to me the main difference of thease two methods?
Number 1. This defines a instance method, that can be used in instances of the model.
Number 2. This defines a class method, and can only be used by the class itself.
Example:
class Lol
def instance_method
end
def self.class_method
end
end
l = Lol.new
l.instance_method #=> This will work
l.class_method #=> This will give you an error
Lol.class_method #=> This will work
The method self.method_name defines the method on the class. Basically within the class definition think of self as referring to the class that is being defined. So when you say def self.method_name you are defining the method on the class itself.
class Foo
def method_name(param)
puts "Instance: #{param}"
end
def self.method_name(param)
puts "Class: #{param}"
end
end
> Foo.new.method_name("bar")
Instance: bar
> Foo.method_name("bar")
Class: bar

Resources