Ruby syntax, semantic questions def status=(status) - ruby-on-rails

I was looking at this code and was trying to figure what def status=(status) means. I have never seen that before.
class Tweet
attr_accessor :status
def initialize(options={})
self.status = options[:status]
end
def public?
self.status && self.status[0] != "#"
end
def status=(status)
#status = status ? status[0...140] : status
end
end

I'll try answering this in layman's terms, since I didn't understand this when starting out.
Let's say you want the Tweet class to have an attribute status. Now you want to change that attribute, well you can't since it's hidden inside the class. The only way you can interact with anything inside a class is by creating a method to do so:
def status=(status)
#status = status # using # makes #status a class instance variable, so you can interact with this attribute in other methods inside this class
end
Great! Now I can do this:
tweet = Tweet.new
tweet.status = "200" # great this works
# now lets get the status back:
tweet.status # blows up!
We can't access the status variable since we haven't defined a method that does that.
def status
#status # returns whatever #status is, will return nil if not set
end
Now tweet.status will work as well.
There are shorthands for this:
attr_setter :status #like the first method
attr_reader :status # like the second one
attr_accessor :status # does both of the above

That is a setter - the method to be called when you say thing.status = whatever.
Without such a method, saying thing.status = whatever would be illegal, since that syntax is merely syntactic sugar for calling the setter.

It means exactly the same thing that def foo always means: define a method named foo.
def initialize
Defines a method named initialize.
def public?
Defines a method named public?
def status=
Defines a method named status=
That's it. There's absolutely nothing special going on here. There is no magic when defining a method whose name ends in an = sign.
The magic happens when calling a method whose name ends in an = sign. Basically, you are allowed to insert whitespace in between the = sign and the rest of the method name. So, instead of having to call the method like this
foo.status= 42
You can call it like this:
foo.status = 42
Which makes it look like an assignment. Note: it is also treated like an assignment in another way; just like with all other forms of assignments, assignment expressions evaluate to the value that is being assigned, which means that the return value of the method is ignored in this case.

Related

Can Struct.new take options as an argument?

I Have a struct - let's say it looks like this:
class MyClass::Subclass < Struct.new(:model1, :model2)
def method1
end
def method2
if model1.active?
end
end
end
This is how I currently have a subclass setup. I am now at a point where I have to pass in a one-time options.
My thinking is that there must be something similar to what you can do in methods like:
class MyClass::Subclass < Struct.new(:model1, :model2, options = {})
def method1
if options["need_this"]
end
end
end
I keep on getting errors:
TypeError: {} is not a symbol
Is there something like options = {} that I can use in Structs? Sorry this may seem like a newb question but Ruby is not my main language.
I'm getting that what you are trying to do is add an optional list of options. (Please correct me if I'm wrong.) To do that, simply add the element. Then in your code, check to see whether or not a hash has been provided:
class MyClass::Subclass < Struct.new(:model1, :model2, :options)
def method1
if self.options && self.options["need_this"]
p 'need this'
end
end
end
x = MyClass::Subclass.new(:x, :y, {'need_this' => 'yesido'})
x.method1 # => 'need this'
You specify Struct parameters as symbols (hence your error), and you can pass any type of arguments you want to them — including none at all. So, while you can't specify a default value, you can check whether a value has been provided; if not, in this case, options will be nil. The example I've put in checks whether a hash has been provided for options, and if so, whether a need_this option has been included in the hash.
Here's the ruby doc on Struct. If you read the overview and the doc on the new method, you should have a clear picture of the Struct syntax.

How to resolve "no implicit conversion of Class into String" in rails?

I have a method in patch as below:
def applicable_resource_type(resource_type)
if resource_type.include?('Student')
student_setting
else
teacher_setting
end
end
This method is called in another patch which checks whether the resource type is 'teacher' or 'student' and stores the boolean value in 'active'.
def exams
School.new(resource: self).request if can_examine_students?
end
private
def can_examine_students?
active = applicable_resource_type(self.class.name).is_active?
if active && (self.is_a?(Teacher))
active = belongs_to_school?
end
active
end
However the resource_type is passed as a String whereas in can_examine_students? it is passed as a class/module. Is there any way to make them both consistent?
I tried the following:
def applicable_resource_type(resource_type)
if resource_type.include?(Student)
student_setting
else
teacher_setting
end
end
But it gave error as:
TypeError:
no implicit conversion of Class into String
I also tried
resource_type.include?('Student'.constantize)
But it gave error same typerror.
Is there a way to resolve the above error and keep both consistent resource_type consistent?
Thanks
Actually, in the second code snippet, when you call applicable_resource_type(self.class.name) you also hand over a String, because class.name returns a string.
If you want to write the first method more elegantly, you can use is_a? which accepts a class name as an argument. It would look like this:
def applicable_resource_type(resource_type)
if resource_type.is_a?(Student)
...
Note, that you pass Student as a class name.
You then have to adapt the second code snippet too and just pass the class and not class.name. Hence,
def can_examine_students?
active = applicable_resource_type(self.class).is_active?
...

Set dynamic values when generating setter methods using attr_accessor in ruby

Is there a better way to set values to setter methods when they are made dynamically using attr_accessor method? I need this for setting values for them from another model in rails. I'm trying to do something like below.
Model_class.all.each do |mdl|
attr_accessor(mdl.some_field)
end
Then I know that it creates a set of get and setter methods. What I want to do is, when these methods are get created, i want some value to be specified for setter method.Thanks in advance.
attr_accessor has no magic embedded. For each of params passed to it, it basically executes something like (the code is simplified and lacks necessary checks etc):
def attr_accessor(*vars)
vars.each do |var|
define_method var { instance_variable_get("##{var}") }
define_method "#{var}=" { |val| instance_variable_set("##{var}", val) }
end
end
That said, the attr_accessor :var1, :var2 DSL simply brings new 4 plain old good ruby methods. For what you are asking, one might take care about defining these methods (or some of them, or none,) themselves. For instance, for cumbersome setting with checks one might do:
attr_reader :variable # reader is reader, no magic
def variable=(val) do
raise ArgumentError, "You must be kidding" if val.nil?
#variable = val
end
The above is called as usual:
instance.variable = 42
#⇒ 42
instance.variable = nil
#⇒ ArgumentError: You must be kidding
Here is another possible implementation for this:
def attr_accessor(*args)
args.each do |attribute|
define_method(attribute.to_sym) { eval("##{attribute}") }
define_method((attribute.to_s + '=').to_sym) {|value| eval("##{attribute} = value") }
end
end

Dynamic method calling with arguments

For example I have class with two methods:
class Example < ActiveRecord::Base
def method_one(value)
end
def method_two
end
end
and method in controller where I call them:
def example
ex = Example.find(params[:id])
ex.send(params[:method], params[:value]) if ex.respond_to?(params[:method])
end
But the problem comes when I try to call method_two
ArgumentError (wrong number of arguments (1 for 0))
It happens because params[:value] returns nil.
The easiest solution is:
def example
ex = Example.find(params[:id])
if ex.respond_to?(params[:method])
if params[:value].present?
ex.send(params[:method], params[:value])
else
ex.send(params[:method])
end
end
end
I wonder if there is any better workaround to do not pass argument if it's null.
What you are trying to do can be really dangerous, so I recommend you filter the params[:method] before.
allowed_methods = {
method_one: ->(ex){ex.method_one(params[:value])}
method_two: ->(ex){ex.method_two}
}
allowed_methods[params[:method]]&.call(ex)
I defined an Hash mapping the methods name to a lambda calling the method, which handles arguments and any special case you want.
I only get a lambda if params[:method] is in the allowed_methods hash as a key.
The &. syntax is the new safe navigation operator in ruby 2.3, and - for short - executes the following method if the receiver is not nil (i.e. the result of allowed_methods[params[:method]])
If you're not using ruby >= 2.3, you can use try instead, which have a similar behavior in this case :
allowed_methods[params[:method]].try(:call, ex)
If you don't filter the value of params[:method], then a user can just pass :destroy for example to delete your entry, which is certainly not what you want.
Also, by calling ex.send ..., you bypass the object's encapsulation, which you usually shouldn't. To use only the public interface, prefer using public_send.
Another point on the big security flaw of you code:
eval is a private method defined on Object (actually inherited from Kernel), so you can use it this way on any object :
object = Object.new
object.send(:eval, '1+1') #=> 2
Now, with your code, imagine the user puts eval as the value of params[:method] and an arbitrary ruby code in params[:value], he can actually do whatever he wants inside your application.
If you understand what you are doing, there are easier workarounds:
def method_two _ = nil
end
or
def method_two *
end
It works as well the other way round:
def method_one *args
end
def method_two *
end
and:
ex.public_send(params[:method], *[params[:value]]) \
if ex.respond_to?(params[:method])
Sidenote: prefer public_send over send unless you are explicitly calling private method.
Using splatted params without modifying the methods signatures:
ex.public_send(*[params[:method], params[:value]].compact)

Ruby limit types of class passed to a method?

What i stumbled upon just now - how to easily limit verity of classes that are passed to one method to only one class type ? ex. code:
class S
attr_reader :s
def initialize(s = nil)
#s = s || 14
end
end
class Gets
def self.read(s)
s.s
end
end
s=S.new
p Gets.read(s) # 14
Let's say that class S has a more complex structure and i want to be sure that only that class could be passed to Gets#read method, how can i do that?
While to solution of sawa definitely is valid and does exactly what you want. In dynamic languages like ruby it's actually more common to use duck typing.
The idea is to just assert to which messages the attribute must respond to. This allows to easily pass in e.g. a different implementation.
class Gets
def self.read(obj)
raise ArgumentError, "must implement #s" unless obj.respond_to?(:s)
obj.s
end
end
class Gets
def self.read(s)
raise ArgumentError unless s.kind_of?(S)
s.s
end
end

Resources